kopia lustrzana https://github.com/bertrik/ttnhabbridge
Move payload decoding into a separate module, add support for another payload format and update unit tests.
rodzic
30496489bf
commit
a448cb50c2
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -80,4 +80,9 @@ public final class Sentence {
|
|||
return formatted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return format();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
Ładowanie…
Reference in New Issue