Move payload decoding into a separate module, add support for another payload format and update unit tests.

koppelting
Bertrik Sikken 2017-08-24 17:35:33 +02:00
rodzic 30496489bf
commit a448cb50c2
6 zmienionych plików z 161 dodań i 73 usunięć

Wyświetl plik

@ -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<HabReceiver> 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());
}

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -80,4 +80,9 @@ public final class Sentence {
return formatted;
}
@Override
public String toString() {
return format();
}
}

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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());
}
}

Wyświetl plik

@ -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);
}
}