Medad Newman 2022-09-10 21:21:29 +01:00 zatwierdzone przez GitHub
commit 7d05938c3c
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
23 zmienionych plików z 330 dodań i 36 usunięć

Wyświetl plik

@ -19,11 +19,12 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import nl.sikken.bertrik.hab.DecodeException;
import nl.sikken.bertrik.hab.EPayloadEncoding;
import nl.sikken.bertrik.hab.ExpiringCache;
import nl.sikken.bertrik.hab.HabReceiver;
import nl.sikken.bertrik.hab.Location;
import nl.sikken.bertrik.hab.PayloadDecoder;
import nl.sikken.bertrik.hab.Sentence;
import nl.sikken.bertrik.hab.habitat.HabReceiver;
import nl.sikken.bertrik.hab.habitat.HabitatUploader;
import nl.sikken.bertrik.hab.habitat.Location;
import nl.sikken.bertrik.hab.amateurSondehub.AmateurSondehubUploader;
import nl.sikken.bertrik.hab.lorawan.HeliumUplinkMessage;
import nl.sikken.bertrik.hab.lorawan.LoraWanUplinkMessage;
import nl.sikken.bertrik.hab.lorawan.LoraWanUplinkMessage.GatewayInfo;
@ -41,6 +42,8 @@ public final class TtnHabBridge {
private final List<MqttListener> mqttListeners = new ArrayList<>();
private final HabitatUploader habUploader;
private final AmateurSondehubUploader amateurSondehubUploader;
private final PayloadDecoder decoder;
private final ExpiringCache gwCache;
@ -90,6 +93,8 @@ public final class TtnHabBridge {
.add(new MqttListener(this::handleMessage, config.getHeliumConfig(), HeliumUplinkMessage.class));
}
this.habUploader = HabitatUploader.create(config.getHabitatConfig());
this.amateurSondehubUploader = AmateurSondehubUploader.create(config.getAmateurSondehubConfig());
this.decoder = new PayloadDecoder(EPayloadEncoding.parse(config.getPayloadEncoding()));
this.gwCache = new ExpiringCache(Duration.ofSeconds(config.getGwCacheExpirationTime()));
}
@ -104,6 +109,7 @@ public final class TtnHabBridge {
// start sub-modules
habUploader.start();
amateurSondehubUploader.start();
for (MqttListener listener : mqttListeners) {
listener.start();
}
@ -119,6 +125,7 @@ public final class TtnHabBridge {
try {
Sentence sentence = decoder.decode(message);
String line = sentence.format();
String amateurSondehubJsonStr = sentence.amateurSondehubFormat();
// collect list of listeners
List<HabReceiver> receivers = new ArrayList<>();
@ -137,6 +144,12 @@ public final class TtnHabBridge {
// send payload telemetry data
habUploader.schedulePayloadTelemetryUpload(line, receivers, now);
// send data to amateurSondehub
amateurSondehubUploader.schedulePayloadTelemetryUpload(amateurSondehubJsonStr, receivers, now);
} catch (DecodeException e) {
LOG.warn("Payload decoding exception: {}", e.getMessage());
} catch (Exception e) {
@ -156,6 +169,7 @@ public final class TtnHabBridge {
listener.stop();
}
habUploader.stop();
amateurSondehubUploader.stop();
LOG.info("Stopped TTN HAB bridge application");
}

Wyświetl plik

@ -2,6 +2,7 @@ package nl.sikken.bertrik;
import com.fasterxml.jackson.annotation.JsonProperty;
import nl.sikken.bertrik.hab.amateurSondehub.AmateurSondehubConfig;
import nl.sikken.bertrik.hab.habitat.HabitatConfig;
import nl.sikken.bertrik.hab.lorawan.MqttConfig;
@ -20,6 +21,9 @@ final class TtnHabBridgeConfig {
@JsonProperty("habitat")
private final HabitatConfig habitatConfig = new HabitatConfig();
@JsonProperty("amateurSondehub")
private final AmateurSondehubConfig amateurSondehubConfig = new AmateurSondehubConfig();
@JsonProperty("gwCacheExpirationTime")
private final int gwCacheExpirationTime = 600; // seconds
@ -34,6 +38,10 @@ final class TtnHabBridgeConfig {
return heliumConfig;
}
public AmateurSondehubConfig getAmateurSondehubConfig() {
return amateurSondehubConfig;
}
public HabitatConfig getHabitatConfig() {
return habitatConfig;
}

Wyświetl plik

@ -1,4 +1,4 @@
package nl.sikken.bertrik.hab.habitat;
package nl.sikken.bertrik.hab;
/**
* Object describing a HAB receiver.

Wyświetl plik

@ -1,4 +1,4 @@
package nl.sikken.bertrik.hab.habitat;
package nl.sikken.bertrik.hab;
/**
* Representation of a HAB receiver location.

Wyświetl plik

@ -87,11 +87,11 @@ public final class PayloadDecoder {
double altitude = sodaq.getAltitude();
Instant instant = Instant.ofEpochSecond(sodaq.getTimeStamp());
Sentence sentence = new Sentence(callSign, counter, instant);
sentence.addField(String.format(Locale.ROOT, "%.6f", latitude));
sentence.addField(String.format(Locale.ROOT, "%.6f", longitude));
sentence.addField(String.format(Locale.ROOT, "%.1f", altitude));
sentence.addField(String.format(Locale.ROOT, "%.0f", sodaq.getBoardTemp()));
sentence.addField(String.format(Locale.ROOT, "%.2f", sodaq.getBattVoltage()));
sentence.addField("lat", String.format(Locale.ROOT, "%.6f", latitude));
sentence.addField("lon", String.format(Locale.ROOT, "%.6f", longitude));
sentence.addField("alt", String.format(Locale.ROOT, "%.1f", altitude));
sentence.addField("temp", String.format(Locale.ROOT, "%.0f", sodaq.getBoardTemp()));
sentence.addField("batt", String.format(Locale.ROOT, "%.2f", sodaq.getBattVoltage()));
return sentence;
} catch (BufferUnderflowException e) {
throw new DecodeException("Error decoding sodaqone", e);
@ -117,15 +117,15 @@ public final class PayloadDecoder {
double longitude = parseDouble(fields.get("lon"));
double altitude = parseDouble(fields.get("gpsalt"));
Sentence sentence = new Sentence(callSign, counter, time);
sentence.addField(String.format(Locale.ROOT, "%.6f", latitude));
sentence.addField(String.format(Locale.ROOT, "%.6f", longitude));
sentence.addField(String.format(Locale.ROOT, "%.1f", altitude));
sentence.addField("lat", String.format(Locale.ROOT, "%.6f", latitude));
sentence.addField("lon", String.format(Locale.ROOT, "%.6f", longitude));
sentence.addField("alt", String.format(Locale.ROOT, "%.1f", altitude));
if (fields.containsKey("temp") && fields.containsKey("vcc")) {
Double temp = parseDouble(fields.get("temp"));
Double vcc = parseDouble(fields.get("vcc"));
sentence.addField(String.format(Locale.ROOT, "%.1f", temp));
sentence.addField(String.format(Locale.ROOT, "%.3f", vcc));
sentence.addField("temp", String.format(Locale.ROOT, "%.1f", temp));
sentence.addField("batt", String.format(Locale.ROOT, "%.3f", vcc));
}
return sentence;
} catch (RuntimeException e) {
@ -167,7 +167,7 @@ public final class PayloadDecoder {
// add all items, in the order they appear in the cayenne message
for (CayenneItem item : cayenne.getItems()) {
for (String s : item.format()) {
sentence.addField(s);
sentence.addField("", s);
}
}

Wyświetl plik

@ -9,6 +9,9 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
@ -22,6 +25,9 @@ public final class Sentence {
private final CrcCcitt16 crc16 = new CrcCcitt16();
ObjectMapper mapper = new ObjectMapper();
ObjectNode json = mapper.createObjectNode();
private final List<String> fields = new ArrayList<>();
/**
@ -42,8 +48,9 @@ public final class Sentence {
*
* @param value the pre-formatted value
*/
public void addField(String value) {
fields.add(value);
public void addField(String fieldName, String fieldValueStr) {
json.put(fieldName, fieldValueStr);
fields.add(fieldValueStr);
}
/**
@ -73,6 +80,23 @@ public final class Sentence {
return String.format(Locale.ROOT, "$$%s*%04X\n", basic, crcValue);
}
public String amateurSondehubFormat() {
// add mandatory fields
json.put("software_name", "ttnhabbridge");
json.put("software_version", "0.0.1");
json.put("uploader_callsign", "foobar");
json.put("modulation", "LoRaWAN");
json.put("time_received", this.time.toString());
json.put("datetime", this.time.toString());
json.put("payload_callsign", this.callSign);
return json.toString();
}
@Override
public String toString() {
return format();

Wyświetl plik

@ -1,4 +1,4 @@
package nl.sikken.bertrik.hab.habitat;
package nl.sikken.bertrik.hab;
import java.util.Locale;

Wyświetl plik

@ -0,0 +1,32 @@
package nl.sikken.bertrik.hab.amateurSondehub;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public final class AmateurSondehubConfig {
@JsonProperty("url")
private final String url;
@JsonProperty("timeout")
private final int timeout;
public AmateurSondehubConfig() {
this("https://api.v2.sondehub.org", 60);
}
public AmateurSondehubConfig(String url, int timeout) {
this.url = url;
this.timeout = timeout;
}
public String getUrl() {
return url;
}
public int getTimeout() {
return timeout;
}
}

Wyświetl plik

@ -0,0 +1,114 @@
package nl.sikken.bertrik.hab.amateurSondehub;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nl.sikken.bertrik.hab.HabReceiver;
import nl.sikken.bertrik.hab.UploadResult;
import okhttp3.OkHttpClient;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.jackson.JacksonConverterFactory;
import retrofit2.converter.scalars.ScalarsConverterFactory;
/**
* AmateurSondehub uploader.
*
* Exchanges data with the amateurSondehub system. Call to ScheduleXXX methods
* are non-blocking. All actions run on a single background thread for
* simplicity.
*/
public final class AmateurSondehubUploader {
private static final Logger LOG = LoggerFactory.getLogger(AmateurSondehubUploader.class);
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final IAmateurSondehubRestApi restClient;
/**
* Creates a new amateurSondehub uploader.
*
* @param config the configuration
* @return the amateurSondehub uploader
*/
public static AmateurSondehubUploader create(AmateurSondehubConfig config) {
LOG.info("Creating new amateurSondehub REST client with timeout {} for {}", config.getTimeout(),
config.getUrl());
Duration timeout = Duration.ofSeconds(config.getTimeout());
OkHttpClient client = new OkHttpClient().newBuilder().callTimeout(timeout).build();
Retrofit retrofit = new Retrofit.Builder().baseUrl(config.getUrl())
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(JacksonConverterFactory.create()).client(client).build();
IAmateurSondehubRestApi restClient = retrofit.create(IAmateurSondehubRestApi.class);
return new AmateurSondehubUploader(restClient);
}
/**
* Constructor.
*
* @param restClient the REST client used for uploading
*/
AmateurSondehubUploader(IAmateurSondehubRestApi restClient) {
this.restClient = restClient;
}
/**
* Starts the uploader process.
*/
public void start() {
LOG.info("Starting amateurSondehub uploader");
}
/**
* Stops the uploader process.
*/
public void stop() {
LOG.info("Stopping amateurSondehub uploader");
executor.shutdown();
}
/**
* Schedules a new sentence to be sent to the HAB network.
*
* @param sentence the ASCII sentence
* @param receivers list of listener that received this sentence
* @param instant the current date/time
*/
public void schedulePayloadTelemetryUpload(String sentence, List<HabReceiver> receivers, Instant instant) {
LOG.info("Uploading for {} receivers: {}", receivers.size(), sentence.trim());
// todo: add receiver to sentence
// submit it to our processing thread
executor.execute(() -> uploadPayloadTelemetry(sentence));
}
/**
* Performs the actual payload telemetry upload as a REST-like call towards
* amateurSondehub.
*
* @param json the JSON payload
*/
private void uploadPayloadTelemetry(String json) {
LOG.info("Upload payload telemetry: {}", json);
try {
Response<UploadResult> response = restClient.uploadDocument(json).execute();
if (response.isSuccessful()) {
LOG.info("Result payload telemetry: {}", response.body());
} else {
LOG.warn("Result payload telemetry: {}", response.message());
}
} catch (IOException e) {
LOG.warn("Caught IOException: {}", e.getMessage());
} catch (Exception e) {
LOG.error("Caught Exception: {}", e);
}
}
}

Wyświetl plik

@ -0,0 +1,16 @@
package nl.sikken.bertrik.hab.amateurSondehub;
import nl.sikken.bertrik.hab.UploadResult;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.PUT;
/**
* Interface definition for payload telemetry and listener telemetry towards
* AmateurSondehub.
*/
public interface IAmateurSondehubRestApi {
@PUT("/amateur/telemetry")
Call<UploadResult> uploadDocument(@Body String document);
}

Wyświetl plik

@ -18,9 +18,11 @@ import javax.xml.bind.DatatypeConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nl.sikken.bertrik.hab.HabReceiver;
import nl.sikken.bertrik.hab.UploadResult;
import nl.sikken.bertrik.hab.habitat.docs.ListenerInformationDoc;
import nl.sikken.bertrik.hab.habitat.docs.ListenerTelemetryDoc;
import nl.sikken.bertrik.hab.habitat.docs.PayloadTelemetryDoc;
import nl.sikken.bertrik.hab.habitat.docs.HabitatPayloadTelemetryDoc;
import okhttp3.OkHttpClient;
import retrofit2.Response;
import retrofit2.Retrofit;
@ -107,7 +109,7 @@ public final class HabitatUploader {
for (HabReceiver receiver : receivers) {
// create Json
PayloadTelemetryDoc doc = new PayloadTelemetryDoc(instant, bytes);
HabitatPayloadTelemetryDoc doc = new HabitatPayloadTelemetryDoc(instant, bytes);
doc.addCallSign(receiver.getCallsign());
String json = doc.format();

Wyświetl plik

@ -1,5 +1,6 @@
package nl.sikken.bertrik.hab.habitat;
import nl.sikken.bertrik.hab.UploadResult;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;

Wyświetl plik

@ -15,7 +15,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
*
* SEE http://habitat.habhub.org/jse/#schemas/payload_telemetry.json
*/
public final class PayloadTelemetryDoc {
public final class HabitatPayloadTelemetryDoc {
private final DateTimeFormatter dateFormat = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
@ -30,7 +30,7 @@ public final class PayloadTelemetryDoc {
* @param instant the creation/upload date/time
* @param rawBytes the raw telemetry string as bytes
*/
public PayloadTelemetryDoc(Instant instant, byte[] rawBytes) {
public HabitatPayloadTelemetryDoc(Instant instant, byte[] rawBytes) {
this.dateCreated = OffsetDateTime.ofInstant(instant, ZoneId.systemDefault());
this.dateUploaded = OffsetDateTime.ofInstant(instant, ZoneId.systemDefault());
this.rawBytes = rawBytes.clone();

Wyświetl plik

@ -6,7 +6,7 @@ import java.util.Locale;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import nl.sikken.bertrik.hab.habitat.HabReceiver;
import nl.sikken.bertrik.hab.HabReceiver;
/**
* Listener information doc.

Wyświetl plik

@ -5,7 +5,7 @@ import java.time.Instant;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import nl.sikken.bertrik.hab.habitat.HabReceiver;
import nl.sikken.bertrik.hab.HabReceiver;
/**
* Listener telemetry doc.

Wyświetl plik

@ -7,7 +7,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import nl.sikken.bertrik.hab.habitat.Location;
import nl.sikken.bertrik.hab.Location;
/**
* LoRaWAN uplink message, stack independent, containing all information needed

Wyświetl plik

@ -17,22 +17,38 @@ public final class SentenceTest {
public void testSentence() {
Instant instant = Instant.ofEpochSecond(0);
Sentence sentence = new Sentence("CALL", 1, instant);
sentence.addField("3.45");
sentence.addField("6.78");
sentence.addField("9.0");
sentence.addField("lon", "3.45");
sentence.addField("lat", "6.78");
sentence.addField("alt", "9.0");
String s = sentence.format();
Assert.assertEquals("$$CALL,1,00:00:00,3.45,6.78,9.0*906C\n", s);
Assert.assertNotNull(sentence.toString());
}
/**
* Verifies basic AmateurSonde json formatting.
*/
@Test
public void testSentenceJson() {
Instant instant = Instant.ofEpochMilli(1652126056001L);
Sentence sentence = new Sentence("CALL", 1, instant);
sentence.addField("lon", "3.45");
sentence.addField("lat", "6.78");
sentence.addField("alt", "9.0");
String s = sentence.amateurSondehubFormat();
Assert.assertEquals("{\"lon\":\"3.45\",\"lat\":\"6.78\",\"alt\":\"9.0\",\"software_name\":\"ttnhabbridge\",\"software_version\":\"0.0.1\",\"uploader_callsign\":\"foobar\",\"modulation\":\"LoRaWAN\",\"time_received\":\"2022-05-09T19:54:16.001Z\",\"datetime\":\"2022-05-09T19:54:16.001Z\",\"payload_callsign\":\"CALL\"}", s);
Assert.assertNotNull(sentence.toString());
}
/**
* Verifies that extra fields are formatted too.
*/
@Test
public void testSentenceExtras() {
Sentence sentence = new Sentence("CALL", 1, Instant.now());
sentence.addField("hello");
sentence.addField("extra" ,"hello");
String s = sentence.format();
Assert.assertTrue(s.contains("hello"));

Wyświetl plik

@ -0,0 +1,62 @@
package nl.sikken.bertrik.hab.amateurSondehub;
import java.io.IOException;
import java.time.Instant;
import java.util.Arrays;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
import nl.sikken.bertrik.hab.HabReceiver;
import nl.sikken.bertrik.hab.UploadResult;
import nl.sikken.bertrik.hab.Location;
import nl.sikken.bertrik.hab.Sentence;
import retrofit2.mock.Calls;
/**
* Unit tests for AmateurSondehubUploader.
*/
public final class AmateurSondehubTest {
private static final Location LOCATION = new Location(52.0162, 4.4735, 5.0);
/**
* Verifies creation of REST client.
*/
@Test
public void testCreateRestClient() {
AmateurSondehubConfig config = new AmateurSondehubConfig();
Assert.assertNotNull(AmateurSondehubUploader.create(config));
}
/**
* Happy flow scenario for payload upload.
*
* @throws IOException
*/
@Test
public void testUploadPayload() throws IOException {
// create a mocked rest client
IAmateurSondehubRestApi restClient = Mockito.mock(IAmateurSondehubRestApi.class);
Mockito.when(restClient.uploadDocument(Mockito.anyString()))
.thenReturn(Calls.response(new UploadResult(true, "id", "rev")));
AmateurSondehubUploader uploader = new AmateurSondehubUploader(restClient);
// verify upload using the uploader
uploader.start();
try {
HabReceiver receiver = new HabReceiver("BERTRIK", LOCATION);
Instant instant = Instant.now();
Sentence sentence = new Sentence("NOTAFLIGHT", 1, instant);
sentence.addField("location", "52.0182307,4.695772,1000");
uploader.schedulePayloadTelemetryUpload(sentence.format(), Arrays.asList(receiver), instant);
Mockito.verify(restClient, Mockito.timeout(3000).times(1)).uploadDocument(Mockito.anyString());
} finally {
uploader.stop();
}
}
}

Wyświetl plik

@ -9,7 +9,10 @@ import org.junit.Ignore;
import org.junit.Test;
import org.mockito.Mockito;
import nl.sikken.bertrik.hab.HabReceiver;
import nl.sikken.bertrik.hab.Location;
import nl.sikken.bertrik.hab.Sentence;
import nl.sikken.bertrik.hab.UploadResult;
import retrofit2.mock.Calls;
/**
@ -49,7 +52,7 @@ public final class HabitatUploaderTest {
HabReceiver receiver = new HabReceiver("BERTRIK", LOCATION);
Instant instant = Instant.now();
Sentence sentence = new Sentence("NOTAFLIGHT", 1, instant);
sentence.addField("52.0182307,4.695772,1000");
sentence.addField("location", "52.0182307,4.695772,1000");
uploader.schedulePayloadTelemetryUpload(sentence.format(), Arrays.asList(receiver), instant);
Mockito.verify(restClient, Mockito.timeout(3000).times(1)).updateListener(Mockito.anyString(),
@ -102,7 +105,7 @@ public final class HabitatUploaderTest {
try {
Instant instant = Instant.now();
Sentence sentence = new Sentence("NOTAFLIGHT", 1, instant);
sentence.addField("52.0182307,4.695772,1000");
sentence.addField("location", "52.0182307,4.695772,1000");
HabReceiver receiver = new HabReceiver("BERTRIK", null);
uploader.schedulePayloadTelemetryUpload(sentence.format(), Arrays.asList(receiver), instant);
Thread.sleep(3000);

Wyświetl plik

@ -3,6 +3,8 @@ package nl.sikken.bertrik.hab.habitat;
import java.time.Instant;
import java.util.Arrays;
import nl.sikken.bertrik.hab.HabReceiver;
import nl.sikken.bertrik.hab.Location;
import nl.sikken.bertrik.hab.Sentence;
public final class RunMultiReceiverTest {

Wyświetl plik

@ -5,8 +5,8 @@ import java.time.Instant;
import org.junit.Assert;
import org.junit.Test;
import nl.sikken.bertrik.hab.habitat.HabReceiver;
import nl.sikken.bertrik.hab.habitat.Location;
import nl.sikken.bertrik.hab.HabReceiver;
import nl.sikken.bertrik.hab.Location;
/**
* Unit tests for ListenerInfoDoc

Wyświetl plik

@ -5,8 +5,8 @@ import java.time.Instant;
import org.junit.Assert;
import org.junit.Test;
import nl.sikken.bertrik.hab.habitat.HabReceiver;
import nl.sikken.bertrik.hab.habitat.Location;
import nl.sikken.bertrik.hab.HabReceiver;
import nl.sikken.bertrik.hab.Location;
/**
* Unit tests for ListenerTelemetryDoc.

Wyświetl plik

@ -16,7 +16,7 @@ public final class PayloadTelemetryDocTest {
@Test
public void testFormat() {
Instant instant = Instant.now();
PayloadTelemetryDoc doc = new PayloadTelemetryDoc(instant, new byte[] {1, 2, 3});
HabitatPayloadTelemetryDoc doc = new HabitatPayloadTelemetryDoc(instant, new byte[] {1, 2, 3});
doc.addCallSign("BERTRIK");
String json = doc.format();