diff --git a/workspace/ttnhabbridge/src/main/java/nl/sikken/bertrik/TtnHabBridge.java b/workspace/ttnhabbridge/src/main/java/nl/sikken/bertrik/TtnHabBridge.java index 9bbf02d..0b8584a 100644 --- a/workspace/ttnhabbridge/src/main/java/nl/sikken/bertrik/TtnHabBridge.java +++ b/workspace/ttnhabbridge/src/main/java/nl/sikken/bertrik/TtnHabBridge.java @@ -3,7 +3,6 @@ package nl.sikken.bertrik; import java.io.File; import java.io.IOException; import java.nio.BufferUnderflowException; -import java.time.Instant; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -14,8 +13,8 @@ import org.slf4j.LoggerFactory; import com.fasterxml.jackson.databind.ObjectMapper; +import nl.sikken.bertrik.hab.PayloadDecoder; import nl.sikken.bertrik.hab.Sentence; -import nl.sikken.bertrik.hab.SodaqOnePayload; import nl.sikken.bertrik.hab.habitat.HabReceiver; import nl.sikken.bertrik.hab.habitat.HabitatUploader; import nl.sikken.bertrik.hab.habitat.IHabitatRestApi; @@ -37,6 +36,7 @@ public final class TtnHabBridge { private final TtnListener ttnListener; private final HabitatUploader habUploader; + private final PayloadDecoder decoder; private final ObjectMapper mapper; /** @@ -66,6 +66,7 @@ public final class TtnHabBridge { HabitatUploader.newRestClient(config.getHabitatUrl(), config.getHabitatTimeout()); this.habUploader = new HabitatUploader(restApi); this.mapper = new ObjectMapper(); + this.decoder = new PayloadDecoder(); } /** @@ -87,29 +88,18 @@ public final class TtnHabBridge { * Handles an incoming TTN message * * @param topic the topic on which the message was received - * @param message the message contents + * @param textMessage the message contents */ - private void handleTTNMessage(String topic, String message) { + private void handleTTNMessage(String topic, String textMessage) { try { - // try to decode the payload - final TtnMessage data = mapper.readValue(message, TtnMessage.class); - final Instant time = data.getMetaData().getTime(); - - final SodaqOnePayload sodaq = SodaqOnePayload.parse(data.getPayload()); - LOG.info("Got SODAQ message: {}", sodaq); - - // construct a sentence - final String callSign = data.getDevId(); - final int id = data.getCounter(); - final double latitude = sodaq.getLatitude(); - final double longitude = sodaq.getLongitude(); - final double altitude = sodaq.getAltitude(); - final Sentence sentence = new Sentence(callSign, id, Date.from(time), latitude, longitude, altitude); + // decode from JSON + final TtnMessage message = mapper.readValue(textMessage, TtnMessage.class); + final Sentence sentence = decoder.decode(message); final String line = sentence.format(); - + // create listeners final List receivers = new ArrayList<>(); - for (TtnMessageGateway gw : data.getMetaData().getMqttGateways()) { + for (TtnMessageGateway gw : message.getMetaData().getMqttGateways()) { final HabReceiver receiver = new HabReceiver(gw.getId(), new Location(gw.getLatitude(), gw.getLongitude(), gw.getAltitude())); receivers.add(receiver); @@ -124,7 +114,7 @@ public final class TtnHabBridge { // send payload telemetry data habUploader.schedulePayloadTelemetryUpload(line, receivers, now); } catch (IOException e) { - LOG.warn("JSON unmarshalling exception '{}' for {}", e.getMessage(), message); + LOG.warn("JSON unmarshalling exception '{}' for {}", e.getMessage(), textMessage); } catch (BufferUnderflowException e) { LOG.warn("Sodaq payload exception: {}", e.getMessage()); } diff --git a/workspace/ttnhabbridge/src/main/java/nl/sikken/bertrik/hab/PayloadDecoder.java b/workspace/ttnhabbridge/src/main/java/nl/sikken/bertrik/hab/PayloadDecoder.java new file mode 100644 index 0000000..848a592 --- /dev/null +++ b/workspace/ttnhabbridge/src/main/java/nl/sikken/bertrik/hab/PayloadDecoder.java @@ -0,0 +1,56 @@ +package nl.sikken.bertrik.hab; + +import java.time.Instant; +import java.util.Date; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +import nl.sikken.bertrik.hab.ttn.TtnMessage; + +/** + * Decodes a payload and encodes it into a UKHAS sentence. + */ +public final class PayloadDecoder { + + private static final Logger LOG = LoggerFactory.getLogger(PayloadDecoder.class); + + /** + * Decodes a TTN message into a UKHAS sentence. + * + * @param message the message as received from TTN + * @return the UKHAS sentence + */ + public Sentence decode(TtnMessage message) { + // common fields + final String callSign = message.getDevId(); + final int id = message.getCounter(); + final Instant time = message.getMetaData().getTime(); + + // decide between two supported specific formats + final ObjectNode fields = message.getPayloadFields(); + if (fields != null) { + LOG.info("Decoding 'ftelkamp' message..."); + + // TTN payload + final double latitude = fields.get("lat").doubleValue(); + final double longitude = fields.get("lon").doubleValue(); + final double altitude = fields.get("gpsalt").doubleValue(); + return new Sentence(callSign, id, Date.from(time), latitude, longitude, altitude); + } else { + LOG.info("Decoding 'sodaqone' message..."); + + // SODAQ payload + final SodaqOnePayload sodaq = SodaqOnePayload.parse(message.getPayloadRaw()); + + // construct a sentence + final double latitude = sodaq.getLatitude(); + final double longitude = sodaq.getLongitude(); + final double altitude = sodaq.getAltitude(); + return new Sentence(callSign, id, Date.from(time), latitude, longitude, altitude); + } + } + +} diff --git a/workspace/ttnhabbridge/src/main/java/nl/sikken/bertrik/hab/Sentence.java b/workspace/ttnhabbridge/src/main/java/nl/sikken/bertrik/hab/Sentence.java index 84b608f..ab3c2fc 100644 --- a/workspace/ttnhabbridge/src/main/java/nl/sikken/bertrik/hab/Sentence.java +++ b/workspace/ttnhabbridge/src/main/java/nl/sikken/bertrik/hab/Sentence.java @@ -80,4 +80,9 @@ public final class Sentence { return formatted; } + @Override + public String toString() { + return format(); + } + } diff --git a/workspace/ttnhabbridge/src/main/java/nl/sikken/bertrik/hab/ttn/TtnMessage.java b/workspace/ttnhabbridge/src/main/java/nl/sikken/bertrik/hab/ttn/TtnMessage.java index 24e3b2b..9d7a802 100644 --- a/workspace/ttnhabbridge/src/main/java/nl/sikken/bertrik/hab/ttn/TtnMessage.java +++ b/workspace/ttnhabbridge/src/main/java/nl/sikken/bertrik/hab/ttn/TtnMessage.java @@ -2,6 +2,7 @@ package nl.sikken.bertrik.hab.ttn; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; /** * Representation of a message received from the TTN MQTT stream. @@ -25,8 +26,11 @@ public final class TtnMessage { private int counter; @JsonProperty("payload_raw") - private byte[] payload; + private byte[] payloadRaw; + @JsonProperty("payload_fields") + private ObjectNode payloadFields; + @JsonProperty("metadata") private TtnMessageMetaData metaData; @@ -50,8 +54,12 @@ public final class TtnMessage { return counter; } - public byte[] getPayload() { - return payload; + public byte[] getPayloadRaw() { + return payloadRaw; + } + + public ObjectNode getPayloadFields() { + return payloadFields; } public TtnMessageMetaData getMetaData() { diff --git a/workspace/ttnhabbridge/src/test/java/nl/sikken/bertrik/hab/PayloadDecoderTest.java b/workspace/ttnhabbridge/src/test/java/nl/sikken/bertrik/hab/PayloadDecoderTest.java new file mode 100644 index 0000000..5b09bb6 --- /dev/null +++ b/workspace/ttnhabbridge/src/test/java/nl/sikken/bertrik/hab/PayloadDecoderTest.java @@ -0,0 +1,78 @@ +package nl.sikken.bertrik.hab; + +import java.io.IOException; + +import org.junit.Assert; +import org.junit.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import nl.sikken.bertrik.hab.Sentence; +import nl.sikken.bertrik.hab.ttn.TtnMessage; + +/** + * 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 + */ + @Test + public void testDecodeJson() throws IOException { + final 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}]}}"; + final TtnMessage message = mapper.readValue(data, TtnMessage.class); + Assert.assertEquals(3, message.getMetaData().getMqttGateways().size()); + + final PayloadDecoder decoder = new PayloadDecoder(); + final Sentence sentence = decoder.decode(message); + + Assert.assertEquals("$$devtrack,1707,19:02:46,51.564211,4.368230,660.0*2BD9", sentence.format().trim()); + } + + /** + * Verifies decoding of some actual MQTT data, in RAW format. + * + * @throws IOException in case of a parse exception + */ + @Test + public void testDecodeRaw() throws IOException { + final String data = "{\"app_id\":\"ttnmapper\",\"dev_id\":\"mapper2\"," + + "\"hardware_serial\":\"0004A30B001ADBC5\",\"port\":1,\"counter\":4," + + "\"payload_raw\":\"loeaWW4T2+8BHzYZzAIeAA8A/QUS\"," + + "\"metadata\":{\"time\":\"2017-08-21T07:11:18.313946438Z\",\"frequency\":868.3," + + "\"modulation\":\"LORA\",\"data_rate\":\"SF7BW125\",\"coding_rate\":\"4/5\"," + + "\"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}]}}"; + + final TtnMessage message = mapper.readValue(data, TtnMessage.class); + + // check gateway field + Assert.assertEquals(27, message.getMetaData().getMqttGateways().get(0).getAltitude(), 0.1); + + // decode payload + final PayloadDecoder decoder = new PayloadDecoder(); + final Sentence sentence = decoder.decode(message); + + Assert.assertEquals("$$mapper2,4,07:11:18,52.022064,4.693023,30.0*22B8", sentence.format().trim()); + } + +} diff --git a/workspace/ttnhabbridge/src/test/java/nl/sikken/bertrik/hab/ttn/TtnMessageTest.java b/workspace/ttnhabbridge/src/test/java/nl/sikken/bertrik/hab/ttn/TtnMessageTest.java deleted file mode 100644 index 877c509..0000000 --- a/workspace/ttnhabbridge/src/test/java/nl/sikken/bertrik/hab/ttn/TtnMessageTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package nl.sikken.bertrik.hab.ttn; - -import java.io.IOException; - -import org.junit.Assert; -import org.junit.Test; - -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -import nl.sikken.bertrik.hab.SodaqOnePayload; - -/** - * Unit tests for TTN messages. - */ -public final class TtnMessageTest { - - private static final String DATA = "{\"app_id\":\"ttnmapper\",\"dev_id\":\"mapper2\"," - + "\"hardware_serial\":\"0004A30B001ADBC5\",\"port\":1,\"counter\":4," - + "\"payload_raw\":\"loeaWW4T2+8BHzYZzAIeAA8A/QUS\"," - + "\"metadata\":{\"time\":\"2017-08-21T07:11:18.313946438Z\",\"frequency\":868.3," - + "\"modulation\":\"LORA\",\"data_rate\":\"SF7BW125\",\"coding_rate\":\"4/5\"," - + "\"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}]}}"; - - /** - * Verifies decoding of some actual MQTT data. - * - * @throws JsonParseException - * @throws JsonMappingException - * @throws IOException - */ - @Test - public void testDecode() throws JsonParseException, JsonMappingException, IOException { - final ObjectMapper mapper = new ObjectMapper(); - final TtnMessage mqttData = mapper.readValue(DATA, TtnMessage.class); - final byte[] raw = mqttData.getPayload(); - - // check gateway field - Assert.assertEquals(27, mqttData.getMetaData().getMqttGateways().get(0).getAltitude(), 0.1); - - // decode payload - final SodaqOnePayload payload = SodaqOnePayload.parse(raw); - Assert.assertNotNull(payload); - } - -}