Refactor / add support for TTN v3 MQTT messages.

ttnv3
Bertrik Sikken 2021-02-28 00:25:10 +01:00
rodzic 53690e19ca
commit 2ae6f9b83d
15 zmienionych plików z 482 dodań i 120 usunięć

Wyświetl plik

@ -13,8 +13,6 @@ import org.eclipse.paho.client.mqttv3.MqttException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import nl.sikken.bertrik.hab.DecodeException;
import nl.sikken.bertrik.hab.EPayloadEncoding;
import nl.sikken.bertrik.hab.ExpiringCache;
@ -25,8 +23,8 @@ import nl.sikken.bertrik.hab.habitat.HabitatUploader;
import nl.sikken.bertrik.hab.habitat.IHabitatRestApi;
import nl.sikken.bertrik.hab.habitat.Location;
import nl.sikken.bertrik.hab.ttn.TtnListener;
import nl.sikken.bertrik.hab.ttn.TtnMessage;
import nl.sikken.bertrik.hab.ttn.TtnMessageGateway;
import nl.sikken.bertrik.hab.ttn.TtnUplinkMessage;
import nl.sikken.bertrik.hab.ttn.TtnUplinkMessage.GatewayInfo;
/**
* Bridge between the-things-network and the habhub network.
@ -40,7 +38,6 @@ public final class TtnHabBridge {
private final TtnListener ttnListener;
private final HabitatUploader habUploader;
private final PayloadDecoder decoder;
private final ObjectMapper mapper;
private final ExpiringCache gwCache;
/**
@ -73,7 +70,6 @@ public final class TtnHabBridge {
IHabitatRestApi restApi =
HabitatUploader.newRestClient(config.getHabitatUrl(), config.getHabitatTimeout());
this.habUploader = new HabitatUploader(restApi);
this.mapper = new ObjectMapper();
this.decoder = new PayloadDecoder(EPayloadEncoding.parse(config.getTtnPayloadEncoding()));
this.gwCache = new ExpiringCache(config.getTtnGwCacheExpiry());
}
@ -95,15 +91,13 @@ public final class TtnHabBridge {
/**
* Handles an incoming TTN message
*
* @param now message arrival time
* @param topic the topic on which the message was received
* @param textMessage the message contents
* @param now message arrival time
*/
private void handleTTNMessage(Instant now, String topic, String textMessage) {
private void handleTTNMessage(TtnUplinkMessage message) {
Instant now = Instant.now();
try {
// decode from JSON
TtnMessage message = mapper.readValue(textMessage, TtnMessage.class);
if (message.isRetry()) {
// skip "retry" messages, they contain duplicate data with a misleading time stamp
LOG.warn("Ignoring 'retry' message");
@ -114,7 +108,7 @@ public final class TtnHabBridge {
// collect list of listeners
List<HabReceiver> receivers = new ArrayList<>();
for (TtnMessageGateway gw : message.getMetaData().getMqttGateways()) {
for (GatewayInfo gw : message.getGateways()) {
String gwName = gw.getId();
Location gwLocation = gw.getLocation();
HabReceiver receiver = new HabReceiver(gwName, gwLocation);
@ -128,8 +122,6 @@ public final class TtnHabBridge {
// send payload telemetry data
habUploader.schedulePayloadTelemetryUpload(line, receivers, now);
} catch (IOException e) {
LOG.warn("JSON unmarshalling exception '{}' for {}", e.getMessage(), textMessage);
} catch (DecodeException e) {
LOG.warn("Payload decoding exception: {}", e.getMessage());
} catch (Exception e) {

Wyświetl plik

@ -13,7 +13,7 @@ import nl.sikken.bertrik.cayenne.CayenneException;
import nl.sikken.bertrik.cayenne.CayenneItem;
import nl.sikken.bertrik.cayenne.CayenneMessage;
import nl.sikken.bertrik.cayenne.ECayennePayloadFormat;
import nl.sikken.bertrik.hab.ttn.TtnMessage;
import nl.sikken.bertrik.hab.ttn.TtnUplinkMessage;
/**
* Decodes a payload and encodes it into a UKHAS sentence.
@ -41,7 +41,7 @@ public final class PayloadDecoder {
* @return the UKHAS sentence
* @throws DecodeException in case of a problem decoding the message
*/
public Sentence decode(TtnMessage message) throws DecodeException {
public Sentence decode(TtnUplinkMessage message) throws DecodeException {
// common fields
String callSign = message.getDevId();
int counter = message.getCounter();
@ -74,7 +74,7 @@ public final class PayloadDecoder {
* @return the UKHAS sentence
* @throws DecodeException in case of a problem decoding the message
*/
private Sentence decodeSodaqOne(TtnMessage message, String callSign, int counter) throws DecodeException {
private Sentence decodeSodaqOne(TtnUplinkMessage message, String callSign, int counter) throws DecodeException {
LOG.info("Decoding 'sodaqone' message...");
try {
@ -107,11 +107,11 @@ public final class PayloadDecoder {
* @return the UKHAS sentence
* @throws DecodeException in case of a problem decoding the message
*/
private Sentence decodeJson(TtnMessage message, String callSign, int counter) throws DecodeException {
private Sentence decodeJson(TtnUplinkMessage message, String callSign, int counter) throws DecodeException {
LOG.info("Decoding 'json' message...");
try {
Instant time = message.getMetaData().getTime();
Instant time = message.getTime();
Map<String, Object> fields = message.getPayloadFields();
double latitude = parseDouble(fields.get("lat"));
double longitude = parseDouble(fields.get("lon"));
@ -154,11 +154,11 @@ public final class PayloadDecoder {
* @return the UKHAS sentence
* @throws DecodeException
*/
private Sentence decodeCayenne(TtnMessage message, String callSign, int counter) throws DecodeException {
private Sentence decodeCayenne(TtnUplinkMessage message, String callSign, int counter) throws DecodeException {
LOG.info("Decoding 'cayenne' message...");
try {
Instant time = message.getMetaData().getTime();
Instant time = message.getTime();
Sentence sentence = new Sentence(callSign, counter, time);
ECayennePayloadFormat cayenneFormat = ECayennePayloadFormat.fromPort(message.getPort());
CayenneMessage cayenne = new CayenneMessage(cayenneFormat);

Wyświetl plik

@ -6,7 +6,6 @@ import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;

Wyświetl plik

@ -1,7 +1,5 @@
package nl.sikken.bertrik.hab.ttn;
import java.time.Instant;
/**
* Interface of the callback from the TTN listener.
*/
@ -9,11 +7,8 @@ public interface IMessageReceived {
/**
* Indicates that a message was received.
*
* @param now the arrival time
* @param topic the topic
* @param message the message
*/
void messageReceived(Instant now, String topic, String message);
void messageReceived(TtnUplinkMessage message);
}

Wyświetl plik

@ -1,7 +1,6 @@
package nl.sikken.bertrik.hab.ttn;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
@ -13,6 +12,9 @@ import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Listener process for receiving data from the TTN.
*/
@ -21,16 +23,18 @@ public final class TtnListener {
private static final Logger LOG = LoggerFactory.getLogger(TtnListener.class);
private static final long DISCONNECT_TIMEOUT_MS = 3000;
private final IMessageReceived callback;
private final MqttClient mqttClient;
private final MqttConnectOptions options;
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* Constructor.
*
* @param callback the listener for a received message.
* @param url the URL of the MQTT server
* @param appId the user name
* @param appKey the password
* @param url the URL of the MQTT server
* @param appId the user name
* @param appKey the password
*/
public TtnListener(IMessageReceived callback, String url, String appId, String appKey) {
LOG.info("Creating client for MQTT server '{}' for app '{}'", url, appId);
@ -39,7 +43,8 @@ public final class TtnListener {
} catch (MqttException e) {
throw new IllegalArgumentException(e);
}
mqttClient.setCallback(new MqttCallbackHandler(mqttClient, "+/devices/+/up", callback));
this.callback = callback;
mqttClient.setCallback(new MqttCallbackHandler(mqttClient, "+/devices/+/up", this::handleMessage));
// create connect options
options = new MqttConnectOptions();
@ -48,6 +53,30 @@ public final class TtnListener {
options.setAutomaticReconnect(true);
}
// notify our caller in a thread safe manner
private void handleMessage(String topic, String payload) {
try {
TtnUplinkMessage uplinkMessage = convertMessage(topic, payload);
callback.messageReceived(uplinkMessage);
} catch (JsonProcessingException e) {
LOG.warn("Caught {}", e.getMessage());
} catch (Throwable e) {
// safety net
LOG.error("Caught unhandled throwable", e);
}
}
// package private for testing
TtnUplinkMessage convertMessage(String topic, String payload) throws JsonProcessingException {
if (topic.startsWith("v3/")) {
Ttnv3UplinkMessage v3message = objectMapper.readValue(payload, Ttnv3UplinkMessage.class);
return v3message.toUplinkMessage();
} else {
Ttnv2UplinkMessage v2message = objectMapper.readValue(payload, Ttnv2UplinkMessage.class);
return v2message.toUplinkMessage();
}
}
/**
* Starts this module.
*
@ -78,9 +107,9 @@ public final class TtnListener {
private final MqttClient client;
private final String topic;
private final IMessageReceived listener;
private final IMqttMessageArrived listener;
private MqttCallbackHandler(MqttClient client, String topic, IMessageReceived listener) {
private MqttCallbackHandler(MqttClient client, String topic, IMqttMessageArrived listener) {
this.client = client;
this.topic = topic;
this.listener = listener;
@ -97,9 +126,8 @@ public final class TtnListener {
// notify our listener, in an exception safe manner
try {
Instant now = Instant.now();
String message = new String(mqttMessage.getPayload(), StandardCharsets.US_ASCII);
listener.messageReceived(now, topic, message);
String json = new String(mqttMessage.getPayload(), StandardCharsets.US_ASCII);
listener.messageArrived(topic, json);
} catch (Exception e) {
LOG.trace("Caught exception", e);
LOG.error("Caught exception in MQTT listener: {}", e.getMessage());
@ -122,4 +150,8 @@ public final class TtnListener {
}
}
interface IMqttMessageArrived {
void messageArrived(String topic, String json);
}
}

Wyświetl plik

@ -22,13 +22,13 @@ public final class TtnMessageGateway {
private String time;
@JsonProperty("latitude")
private Double latitude;
private Double latitude = Double.NaN;
@JsonProperty("longitude")
private Double longitude;
private Double longitude = Double.NaN;
@JsonProperty("altitude")
private Double altitude;
private Double altitude = Double.NaN;
private TtnMessageGateway() {
// jackson constructor

Wyświetl plik

@ -1,6 +1,7 @@
package nl.sikken.bertrik.hab.ttn;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@ -13,10 +14,10 @@ import com.fasterxml.jackson.annotation.JsonProperty;
public final class TtnMessageMetaData {
@JsonProperty("time")
private String time;
private String time = "";
@JsonProperty("gateways")
private List<TtnMessageGateway> gateways;
private List<TtnMessageGateway> gateways = new ArrayList<>();
private TtnMessageMetaData() {
// empty jackson constructor

Wyświetl plik

@ -0,0 +1,100 @@
package nl.sikken.bertrik.hab.ttn;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import nl.sikken.bertrik.hab.habitat.Location;
/**
* Uplink message, TTN stack version independent, containing all information needed to create a habhub sentence
*/
public final class TtnUplinkMessage {
private final Instant time;
private final String appId;
private final String deviceId;
private final int counter;
private final int port;
private final Map<String, Object> payloadFields = new HashMap<>();
private final byte[] payloadRaw;
private final boolean isRetry;
private final List<GatewayInfo> gateways = new ArrayList<>();
public TtnUplinkMessage(Instant time, String appId, String deviceId, int counter, byte[] payloadRaw, boolean isRetry) {
this.time = time;
this.appId = appId;
this.deviceId = deviceId;
this.counter = counter;
this.port = 1; // TODO port
this.payloadRaw = payloadRaw.clone();
this.isRetry = isRetry;
}
public void addField(String name, Object value) {
payloadFields.put(name, value);
}
public Instant getTime() {
return time;
}
public String getAppId() {
return appId;
}
public String getDevId() {
return deviceId;
}
public int getCounter() {
return counter;
}
public byte[] getPayloadRaw() {
return payloadRaw.clone();
}
public Map<String, Object> getPayloadFields() {
return new HashMap<>(payloadFields);
}
public boolean isRetry() {
return isRetry;
}
public int getPort() {
return port;
}
public void addGateway(String id, double lat, double lon, double alt) {
gateways.add(new GatewayInfo(id, new Location(lat, lon, alt)));
}
public List<GatewayInfo> getGateways() {
return gateways;
}
public static final class GatewayInfo {
private final String id;
private final Location location;
public GatewayInfo(String id, Location location) {
this.id = id;
this.location = location;
}
public String getId() {
return id;
}
public Location getLocation() {
return location;
}
}
}

Wyświetl plik

@ -10,7 +10,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
* Representation of a message received from the TTN MQTT stream.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public final class TtnMessage {
public final class Ttnv2UplinkMessage {
@JsonProperty("app_id")
private String appId;
@ -29,29 +29,29 @@ public final class TtnMessage {
@JsonProperty("is_retry")
private boolean isRetry;
@JsonProperty("payload_raw")
private byte[] payloadRaw = new byte[0];
@JsonProperty("payload_fields")
private Map<String, Object> payloadFields = new HashMap<>();
@JsonProperty("metadata")
private TtnMessageMetaData metaData;
private TtnMessage() {
// Jackson constructor
private Ttnv2UplinkMessage() {
// Jackson constructor
}
// constructor for testing
public TtnMessage(String devId, int counter, TtnMessageMetaData metaData, byte[] payloadRaw) {
this();
this.devId = devId;
this.counter = counter;
this.metaData = metaData;
this.payloadRaw = payloadRaw.clone();
public Ttnv2UplinkMessage(String devId, int counter, TtnMessageMetaData metaData, byte[] payloadRaw) {
this();
this.devId = devId;
this.counter = counter;
this.metaData = metaData;
this.payloadRaw = payloadRaw.clone();
}
public String getAppId() {
return appId;
}
@ -67,7 +67,7 @@ public final class TtnMessage {
public int getPort() {
return port;
}
public boolean isRetry() {
return isRetry;
}
@ -88,4 +88,12 @@ public final class TtnMessage {
return metaData;
}
public TtnUplinkMessage toUplinkMessage() {
TtnUplinkMessage message = new TtnUplinkMessage(metaData.getTime(), appId, devId, counter, payloadRaw, isRetry);
for (TtnMessageGateway gw : metaData.getMqttGateways()) {
message.addGateway(gw.getId(), gw.getLatitude(), gw.getLongitude(), gw.getAltitude());
}
return message;
}
}

Wyświetl plik

@ -0,0 +1,114 @@
package nl.sikken.bertrik.hab.ttn;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Representation of the TTNv3 uplink message.<br>
* <br>
* This is purely a data structure, so all fields are public for easy access.
* All sub-structures are contained in this file too.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public final class Ttnv3UplinkMessage {
@JsonProperty("end_device_ids")
EndDeviceIds endDeviceIds = new EndDeviceIds();
@JsonProperty("received_at")
String receivedAt = "";
@JsonProperty("uplink_message")
UplinkMessage uplinkMessage = new UplinkMessage();
@JsonIgnoreProperties(ignoreUnknown = true)
final static class EndDeviceIds {
@JsonProperty("device_id")
String deviceId = "";
@JsonProperty("application_ids")
ApplicationIds applicationIds = new ApplicationIds();
@JsonProperty("dev_eui")
String deviceEui = "";
@JsonProperty("join_eui")
String joinEui = "";
@JsonProperty("dev_addr")
String deviceAddress = "";
}
final static class ApplicationIds {
@JsonProperty("application_id")
String applicationId = "";
}
@JsonIgnoreProperties(ignoreUnknown = true)
final static class UplinkMessage {
@JsonProperty("f_port")
int fport = 0;
@JsonProperty("f_cnt")
int fcnt = 0;
@JsonProperty("frm_payload")
byte[] payload = new byte[0];
@JsonProperty("rx_metadata")
List<RxMetadata> rxMetadata = new ArrayList<>();
}
@JsonIgnoreProperties(ignoreUnknown = true)
final static class RxMetadata {
@JsonProperty("gateway_ids")
final GatewayIds gatewayIds = new GatewayIds();
@JsonProperty("location")
final Location location = new Location();
}
@JsonIgnoreProperties(ignoreUnknown = true)
final static class GatewayIds {
@JsonProperty("gateway_id")
String gatewayId = "";
@JsonProperty("eui")
String eui = "";
}
final static class Location {
@JsonProperty("latitude")
private double latitude = Double.NaN;
@JsonProperty("longitude")
private double longitude = Double.NaN;
@JsonProperty("altitude")
private double altitude = Double.NaN;
@JsonProperty("source")
private String source = "";
}
public TtnUplinkMessage toUplinkMessage() {
TtnUplinkMessage uplink = new TtnUplinkMessage(Instant.parse(receivedAt),
endDeviceIds.applicationIds.applicationId, endDeviceIds.deviceId, uplinkMessage.fcnt,
uplinkMessage.payload, false);
for (RxMetadata metadata : uplinkMessage.rxMetadata) {
String id = metadata.gatewayIds.gatewayId;
if (id.isBlank()) {
id = metadata.gatewayIds.eui;
}
uplink.addGateway(id, metadata.location.latitude, metadata.location.longitude, metadata.location.altitude);
}
return uplink;
}
}

Wyświetl plik

@ -1,8 +1,8 @@
package nl.sikken.bertrik.hab;
import java.io.IOException;
import java.time.Instant;
import java.util.Base64;
import java.util.Collections;
import org.junit.Assert;
import org.junit.Test;
@ -11,52 +11,20 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import nl.sikken.bertrik.hab.ttn.TtnMessage;
import nl.sikken.bertrik.hab.ttn.TtnMessageMetaData;
import nl.sikken.bertrik.hab.ttn.TtnUplinkMessage;
import nl.sikken.bertrik.hab.ttn.Ttnv2UplinkMessage;
/**
* Unit tests for PayloadDecoder.
*/
public final class PayloadDecoderTest {
private static final ObjectMapper mapper = new ObjectMapper();
/**
* Verifies decoding of some actual MQTT data, in JSON format.
*
* @throws IOException in case of a parse exception
* @throws DecodeException in case of a decode exception
*/
@Test
public void testDecodeJson() throws IOException, DecodeException {
String data = "{\"app_id\":\"spaceballoon\",\"dev_id\":\"devtrack\","
+ "\"hardware_serial\":\"00490B7EB25521E6\",\"port\":1,\"counter\":1707,"
+ "\"payload_raw\":\"AAsm1AxMAUEAY/wCNh68EwKaihEClAEAAwAF\",\"payload_fields\":{\"baralt\":321,"
+ "\"gpsalt\":660,\"hacc\":3,\"hpa\":994,\"lat\":51.5642112,\"lon\":4.3682304,\"mode\":0,\"rssi\":-99,"
+ "\"sats\":17,\"seq\":565,\"slot\":1,\"snr\":-4,\"temp\":1.1,\"type\":\"stat\",\"vacc\":5,"
+ "\"vcc\":3.148},\"metadata\":{\"time\":\"2017-03-24T19:02:46.316523288Z\",\"frequency\":867.1,"
+ "\"modulation\":\"LORA\",\"data_rate\":\"SF7BW125\",\"coding_rate\":\"4/5\","
+ "\"gateways\":[{\"gtw_id\":\"eui-008000000000b8b6\",\"timestamp\":1250904307,"
+ "\"time\":\"2017-03-24T19:02:46.338171Z\",\"channel\":3,\"rssi\":-120,\"snr\":-8.5,"
+ "\"latitude\":52.0182,\"longitude\":4.7084384},{\"gtw_id\":\"eui-008000000000b706\","
+ "\"timestamp\":3407032963,\"time\":\"\",\"channel\":3,\"rssi\":-120,\"snr\":-3,"
+ "\"latitude\":51.57847,\"longitude\":4.4564,\"altitude\":4},{\"gtw_id\":\"eui-aa555a00080e0096\","
+ "\"timestamp\":422749595,\"time\":\"2017-03-24T19:02:45.89182Z\",\"channel\":3,\"rssi\":-118,"
+ "\"snr\":-0.8}]}}";
TtnMessage message = mapper.readValue(data, TtnMessage.class);
Assert.assertEquals(3, message.getMetaData().getMqttGateways().size());
PayloadDecoder decoder = new PayloadDecoder(EPayloadEncoding.JSON);
Sentence sentence = decoder.decode(message);
Assert.assertEquals("$$devtrack,1707,19:02:46,51.564211,4.368230,660.0,1.1,3.148*B35B",
sentence.format().trim());
}
/**
* Verifies decoding of some actual MQTT data, in RAW format.
*
* @throws IOException in case of a parse exception
* @throws IOException in case of a parse exception
* @throws DecodeException in case of a decode exception
*/
@Test
@ -69,23 +37,25 @@ public final class PayloadDecoderTest {
+ "\"gateways\":[{\"gtw_id\":\"eui-008000000000b8b6\",\"timestamp\":1409115451,"
+ "\"time\":\"2017-08-21T07:11:18.338662Z\",\"channel\":1,\"rssi\":-114,\"snr\":-0.2,"
+ "\"rf_chain\":1,\"latitude\":52.0182,\"longitude\":4.70844,\"altitude\":27}]}}";
TtnMessage message = mapper.readValue(data, TtnMessage.class);
Ttnv2UplinkMessage message = mapper.readValue(data, Ttnv2UplinkMessage.class);
TtnUplinkMessage uplink = message.toUplinkMessage();
// check gateway field
Assert.assertEquals(27, message.getMetaData().getMqttGateways().get(0).getAltitude(), 0.1);
// decode payload
PayloadDecoder decoder = new PayloadDecoder(EPayloadEncoding.SODAQ_ONE);
Sentence sentence = decoder.decode(message);
Sentence sentence = decoder.decode(uplink);
Assert.assertEquals("$$mapper2,4,07:11:18,52.022064,4.693023,30.0,19,4.10*81FD", sentence.format().trim());
}
/**
* Verifies decoding of some actual MQTT data, in cayenne format (fix applied to analog input).
* Verifies decoding of some actual MQTT data, in cayenne format (fix applied to
* analog input).
*
* @throws IOException in case of a parse exception
* @throws IOException in case of a parse exception
* @throws DecodeException in case of a decode exception
*/
@Test
@ -97,15 +67,16 @@ public final class PayloadDecoderTest {
+ "\"gateways\":[{\"gtw_id\":\"eui-008000000000b8b6\",\"timestamp\":2382048707,"
+ "\"time\":\"2017-09-08T16:53:10.342388Z\",\"channel\":0,\"rssi\":-119,\"snr\":-2,\"rf_chain\":1,"
+ "\"latitude\":52.0182,\"longitude\":4.70844,\"altitude\":27}]}}";
TtnMessage message = mapper.readValue(data, TtnMessage.class);
Ttnv2UplinkMessage message = mapper.readValue(data, Ttnv2UplinkMessage.class);
TtnUplinkMessage uplink = message.toUplinkMessage();
// decode payload
PayloadDecoder decoder = new PayloadDecoder(EPayloadEncoding.CAYENNE);
Sentence sentence = decoder.decode(message);
Sentence sentence = decoder.decode(uplink);
Assert.assertEquals("$$ttntest1,9,16:53:10,52.0220,4.6927,44.00,4.21,29.0*383E\n", sentence.format());
}
/**
* Verifies that an unknown payload encoding is detected.
*/
@ -114,7 +85,7 @@ public final class PayloadDecoderTest {
PayloadDecoder decoder = new PayloadDecoder(null);
Assert.assertNotNull(decoder);
}
/**
* Verifies decoding of another set of actual payload data.
*
@ -122,27 +93,27 @@ public final class PayloadDecoderTest {
*/
@Test
public void testCayenne2() throws DecodeException {
TtnMessageMetaData metaData = new TtnMessageMetaData("2020-02-05T22:00:58.930936Z", Collections.emptyList());
TtnMessage message = new TtnMessage("test", 123, metaData,
Base64.getDecoder().decode("AYgH1ecAzV4AC7gCZwArAwIBhg=="));
TtnUplinkMessage message = new TtnUplinkMessage(Instant.parse("2020-02-05T22:00:58.930936Z"), "test", "test",
123, Base64.getDecoder().decode("AYgH1ecAzV4AC7gCZwArAwIBhg=="), false);
// decode payload
PayloadDecoder decoder = new PayloadDecoder(EPayloadEncoding.CAYENNE);
Sentence sentence = decoder.decode(message);
Assert.assertEquals("$$test,123,22:00:58,51.3511,5.2574,30.00,4.3,3.90*A07E\n", sentence.format());
}
@Test
public void testCayenneWithFields() throws DecodeException, JsonMappingException, JsonProcessingException {
String json = "{\"app_id\":\"ttn-arduino-tracker-swallow\",\"dev_id\":\"ttnwiv2n\",\"hardware_serial\":\"003B0C6BF8C3B76E\",\"port\":1,\"counter\":170,\"payload_raw\":\"AYgHr8T/Yr4AIaI=\",\r\n" +
"\"payload_fields\":{\"gps_1\":{\"altitude\":86.1,\"latitude\":50.3748,\"longitude\":-4.0258}},\"metadata\":{\"time\":\"2020-08-17T17:51:05.573485776Z\",\"frequency\":868.5,\"modulation\":\r\n" +
"\"LORA\",\"data_rate\":\"SF7BW125\",\"airtime\":61696000,\"coding_rate\":\"4/5\",\"gateways\":[{\"gtw_id\":\"eui-58a0cbfffe801f61\",\"timestamp\":141113011,\"time\":\"2020-08-17T17:51:05.438510894Z\",\r\n" +
"\"channel\":0,\"rssi\":-34,\"snr\":7.75,\"rf_chain\":0}]}}";
String json = "{\"app_id\":\"ttn-arduino-tracker-swallow\",\"dev_id\":\"ttnwiv2n\",\"hardware_serial\":\"003B0C6BF8C3B76E\",\"port\":1,\"counter\":170,\"payload_raw\":\"AYgHr8T/Yr4AIaI=\",\r\n"
+ "\"payload_fields\":{\"gps_1\":{\"altitude\":86.1,\"latitude\":50.3748,\"longitude\":-4.0258}},\"metadata\":{\"time\":\"2020-08-17T17:51:05.573485776Z\",\"frequency\":868.5,\"modulation\":\r\n"
+ "\"LORA\",\"data_rate\":\"SF7BW125\",\"airtime\":61696000,\"coding_rate\":\"4/5\",\"gateways\":[{\"gtw_id\":\"eui-58a0cbfffe801f61\",\"timestamp\":141113011,\"time\":\"2020-08-17T17:51:05.438510894Z\",\r\n"
+ "\"channel\":0,\"rssi\":-34,\"snr\":7.75,\"rf_chain\":0}]}}";
ObjectMapper mapper = new ObjectMapper();
TtnMessage message = mapper.readValue(json, TtnMessage.class);
Ttnv2UplinkMessage message = mapper.readValue(json, Ttnv2UplinkMessage.class);
TtnUplinkMessage uplink = message.toUplinkMessage();
PayloadDecoder decoder = new PayloadDecoder(EPayloadEncoding.CAYENNE);
Sentence sentence = decoder.decode(message);
Sentence sentence = decoder.decode(uplink);
Assert.assertNotNull(sentence);
Assert.assertEquals("$$ttnwiv2n,170,17:51:05,50.3748,-4.0258,86.10*6647\n", sentence.format());
}
}

Wyświetl plik

@ -26,7 +26,7 @@ public final class TtnMessageTest {
@Test
public void testUplink() throws JsonParseException, JsonMappingException, IOException {
InputStream is = getClass().getClassLoader().getResourceAsStream("uplink_nominal.json");
TtnMessage message = mapper.readValue(is, TtnMessage.class);
Ttnv2UplinkMessage message = mapper.readValue(is, Ttnv2UplinkMessage.class);
Assert.assertEquals(false, message.isRetry());
}
@ -36,7 +36,7 @@ public final class TtnMessageTest {
@Test
public void testUplinkWithRetry() throws JsonParseException, JsonMappingException, IOException {
InputStream is = getClass().getClassLoader().getResourceAsStream("uplink_with_retry.json");
TtnMessage message = mapper.readValue(is, TtnMessage.class);
Ttnv2UplinkMessage message = mapper.readValue(is, Ttnv2UplinkMessage.class);
Assert.assertEquals(true, message.isRetry());
}

Wyświetl plik

@ -0,0 +1,49 @@
package nl.sikken.bertrik.hab.ttn;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import nl.sikken.bertrik.hab.ttn.TtnUplinkMessage.GatewayInfo;
public final class Ttnv2UplinkMessageTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* Tests decoding of a JSON uplink message.
*/
@Test
public void testDecode() throws JsonMappingException, JsonProcessingException {
String data = "{\"app_id\":\"habhub\",\"dev_id\":\"ttntest1\",\"hardware_serial\":\"0004A30B001ADBC5\","
+ "\"port\":1,\"counter\":9,\"payload_raw\":\"AYgH8BwAt08AETACAgGlA2cBIg==\","
+ "\"metadata\":{\"time\":\"2017-09-08T16:53:10.446526987Z\",\"frequency\":868.1,"
+ "\"modulation\":\"LORA\",\"data_rate\":\"SF7BW125\",\"coding_rate\":\"4/5\","
+ "\"gateways\":[{\"gtw_id\":\"eui-008000000000b8b6\",\"timestamp\":2382048707,"
+ "\"time\":\"2017-09-08T16:53:10.342388Z\",\"channel\":0,\"rssi\":-119,\"snr\":-2,\"rf_chain\":1,"
+ "\"latitude\":52.0182,\"longitude\":4.70844,\"altitude\":27}]}}";
Ttnv2UplinkMessage message = MAPPER.readValue(data, Ttnv2UplinkMessage.class);
Assert.assertNotNull(message);
TtnUplinkMessage uplinkMessage = message.toUplinkMessage();
Assert.assertEquals("habhub", uplinkMessage.getAppId());
Assert.assertEquals("ttntest1", uplinkMessage.getDevId());
Assert.assertEquals(1, uplinkMessage.getPort());
Assert.assertEquals(9, uplinkMessage.getCounter());
List<GatewayInfo> gateways = uplinkMessage.getGateways();
GatewayInfo gw = gateways.get(0);
Assert.assertEquals("eui-008000000000b8b6", gw.getId());
Assert.assertEquals(52.0182, gw.getLocation().getLat(), 1E-4);
Assert.assertEquals(4.70844, gw.getLocation().getLon(), 1E-4);
Assert.assertEquals(27, gw.getLocation().getAlt(), 1E-1);
}
}

Wyświetl plik

@ -0,0 +1,38 @@
package nl.sikken.bertrik.hab.ttn;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
import nl.sikken.bertrik.hab.ttn.TtnUplinkMessage.GatewayInfo;
public final class Ttnv3UplinkMessageTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
@Test
public void testDecode() throws IOException {
InputStream is = getClass().getClassLoader().getResourceAsStream("ttnv3_uplink.json");
Ttnv3UplinkMessage message = MAPPER.readValue(is, Ttnv3UplinkMessage.class);
TtnUplinkMessage uplinkMessage = message.toUplinkMessage();
Assert.assertEquals("test2id", uplinkMessage.getAppId());
Assert.assertEquals("v3demo1", uplinkMessage.getDevId());
Assert.assertEquals(1, uplinkMessage.getPort());
Assert.assertEquals(84, uplinkMessage.getCounter());
List<GatewayInfo> gateways = uplinkMessage.getGateways();
GatewayInfo gw = gateways.get(0);
Assert.assertEquals("eui-024b08fefe040083", gw.getId());
Assert.assertEquals(52.0, gw.getLocation().getLat(), 0.1);
Assert.assertEquals(4.7, gw.getLocation().getLon(), 0.1);
Assert.assertFalse(Double.isFinite(gw.getLocation().getAlt()));
}
}

Wyświetl plik

@ -0,0 +1,63 @@
{
"end_device_ids": {
"device_id": "v3demo1",
"application_ids": {
"application_id": "test2id"
},
"dev_eui": "008000000000A0B6",
"join_eui": "0000000000000000",
"dev_addr": "260B850F"
},
"correlation_ids": [
"as:up:01EY5RKC5HCVMCVAHPN8EMZQQC",
"gs:conn:01EY5NK2FWERHS4M75W8QSANK9",
"gs:up:host:01EY5NK2G2MCRVMKG9015JJ0GT",
"gs:uplink:01EY5RKBZ1XJJA112CDY5BSQKQ",
"ns:uplink:01EY5RKBZ3YRCCCPRQCAY2YQ4Y",
"rpc:/ttn.lorawan.v3.GsNs/HandleUplink:01EY5RKBZ2DW9HCP0AZ6XQ1C4V",
"rpc:/ttn.lorawan.v3.NsAs/HandleUplink:01EY5RKC5GT79XR6EXRZ4HBYXK"
],
"received_at": "2021-02-10T10:43:12.689616958Z",
"uplink_message": {
"session_key_id": "AXeLWcyXpwXNMEKFxfTSqQ==",
"f_port": 1,
"f_cnt": 84,
"frm_payload": "eyJ0IjoyNC40fQ==",
"rx_metadata": [
{
"gateway_ids": {
"gateway_id": "eui-024b08fefe040083",
"eui": "E024B08FEFE04008"
},
"time": "2021-02-10T10:43:12.461088Z",
"timestamp": 3157838364,
"rssi": -113,
"channel_rssi": -113,
"snr": -3,
"location": {
"latitude": 52.00996862975038,
"longitude": 4.716007411479951,
"source": "SOURCE_REGISTRY"
},
"uplink_token": "CiIKIAoUZXVpLTAyNGIwOGZlZmUwNDAwODMSCOAksI/v4EAIEJyU4+ELGgwIwPGOgQYQ97bR5QEg4PrV7vNb",
"channel_index": 5
}
],
"settings": {
"data_rate": {
"lora": {
"bandwidth": 125000,
"spreading_factor": 7
}
},
"data_rate_index": 5,
"coding_rate": "4/5",
"frequency": "867500000",
"timestamp": 3157838364,
"time": "2021-02-10T10:43:12.461088Z"
},
"received_at": "2021-02-10T10:43:12.483085362Z",
"consumed_airtime": "0.061696s"
}
}