kopia lustrzana https://github.com/bertrik/ttnhabbridge
Refactor / add support for TTN v3 MQTT messages.
rodzic
53690e19ca
commit
2ae6f9b83d
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()));
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
Ładowanie…
Reference in New Issue