diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/GatewayInfo.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/GatewayInfo.java new file mode 100644 index 0000000000..bffa8730d0 --- /dev/null +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/GatewayInfo.java @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2020 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + + +package org.eclipse.hono.adapter.lora; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A container for meta information about a Lora gateway. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class GatewayInfo { + + @JsonProperty(LoraConstants.APP_PROPERTY_SNR) + private Double snr; + @JsonProperty(LoraConstants.APP_PROPERTY_RSS) + private Integer rssi; + @JsonProperty(LoraConstants.GATEWAY_ID) + private String gatewayId; + @JsonProperty(LoraConstants.APP_PROPERTY_CHANNEL) + private Integer channel; + @JsonProperty(LoraConstants.LOCATION) + private Location location; + + /** + * Gets the gateway's identifier. + * + * @return The identifier or {@code null} if unknown. + * @throws NullPointerException if id is {@code null}. + */ + public String getGatewayId() { + return gatewayId; + } + + /** + * Sets the gateway's identifier. + * + * @param id The identifier or {@code null} if unknown. + * @throws NullPointerException if id is {@code null}. + */ + public void setGatewayId(final String id) { + this.gatewayId = Objects.requireNonNull(id); + } + + /** + * Gets the concentrator IF channel that the gateway used for receiving + * the data. + * + * @return The channel or {@code null} if unknown. + */ + public Integer getChannel() { + return channel; + } + + /** + * Sets the concentrator IF channel that the gateway used for receiving + * the data. + * + * @param channel The channel or {@code null} if unknown. + * @return This object for command chaining. + */ + public GatewayInfo setChannel(final Integer channel) { + this.channel = channel; + return this; + } + + /** + * Gets the location of the receiving gateway. + * + * @return The location or {@code null} if unknown. + */ + public Location getLocation() { + return location; + } + + /** + * Sets the location of the receiving gateway. + * + * @param location The location or {@code null} if unknown. + * @return This object for command chaining. + */ + public GatewayInfo setLocation(final Location location) { + this.location = location; + return this; + } + + /** + * Gets the signal-to-noise ratio (SNR) detected by the + * gateway when receiving the data. + * + * @return The ratio in dB or {@code null} if unknown. + */ + public Double getSnr() { + return snr; + } + + /** + * Sets the signal-to-noise ratio (SNR) detected by the + * gateway when receiving the data. + * + * @param snr The ratio in dB or {@code null} if unknown. + * @return This object for command chaining. + */ + public GatewayInfo setSnr(final Double snr) { + this.snr = snr; + return this; + } + + /** + * Gets the received signal strength indicator (RSSI) detected by the + * gateway when receiving the data. + * + * @return The RSSI value in dBm or {@code null} if unknown. + */ + public Integer getRssi() { + return rssi; + } + + /** + * Sets the received signal strength indicator (RSSI) detected by the + * gateway when receiving the data. + * + * @param rssi The RSSI value in dBm or {@code null} if unknown. + * @return This object for command chaining. + * @throws IllegalArgumentException if the rssi value is positive. + */ + public GatewayInfo setRssi(final Integer rssi) { + if (rssi != null && rssi.intValue() > 0) { + throw new IllegalArgumentException("RSSI value must be a negative integer"); + } + this.rssi = rssi; + return this; + } +} diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/Location.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/Location.java new file mode 100644 index 0000000000..f760bc032d --- /dev/null +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/Location.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2020 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + + +package org.eclipse.hono.adapter.lora; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A 3D geo-location that consists of longitude, latitude and (optional) altitude. + * + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class Location { + + @JsonProperty(LoraConstants.APP_PROPERTY_FUNCTION_LONGITUDE) + private Double longitude; + @JsonProperty(LoraConstants.APP_PROPERTY_FUNCTION_LATITUDE) + private Double latitude; + @JsonProperty(LoraConstants.APP_PROPERTY_FUNCTION_ALTITUDE) + private Double altitude; + + /** + * Creates a new location for coordinates. + * + * @param longitude The longitude in decimal degrees. + * @param latitude The latitude in decimal degrees. + * @param altitude The altitude in meters or {@code null} if unknown. + * @throws NullPointerException if longitude or latitude are {@code null}. + */ + public Location( + @JsonProperty(value = LoraConstants.APP_PROPERTY_FUNCTION_LONGITUDE, required = true) final Double longitude, + @JsonProperty(value = LoraConstants.APP_PROPERTY_FUNCTION_LATITUDE, required = true) final Double latitude, + @JsonProperty(LoraConstants.APP_PROPERTY_FUNCTION_ALTITUDE) final Double altitude) { + this.longitude = Objects.requireNonNull(longitude); + this.latitude = Objects.requireNonNull(latitude); + this.altitude = altitude; + } + + /** + * Gets the longitude of the location. + * + * @return The longitude in decimal degrees. + */ + public Double getLongitude() { + return longitude; + } + + /** + * Gets the latitude of the location. + * + * @return The latitude in decimal degrees. + */ + public Double getLatitude() { + return latitude; + } + + /** + * Gets the altitude of the location. + * + * @return The altitude in meters. + */ + public Double getAltitude() { + return altitude; + } +} diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/LoraConstants.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/LoraConstants.java index 3db4ade993..e316166820 100644 --- a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/LoraConstants.java +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/LoraConstants.java @@ -23,6 +23,7 @@ public class LoraConstants { * which an uploaded message has originally been received. */ public static final String APP_PROPERTY_ORIG_LORA_PROVIDER = "orig_lora_provider"; + public static final String FIELD_PSK = "psk"; public static final String FIELD_VIA = "via"; public static final String FIELD_AUTH_ID = "auth-id"; @@ -37,8 +38,9 @@ public class LoraConstants { public static final String EMPTY = ""; public static final String CONTENT_TYPE_LORA_POST_FIX = "+json"; public static final String CONTENT_TYPE_LORA_BASE = "application/vnd.eclipse-hono.lora."; - public static final String NORMALIZED_PROPERTIES = "normalized_properties"; + public static final String META_DATA = "meta_data"; public static final String ADDITIONAL_DATA = "additional_data"; + public static final String APP_PROPERTY_RSS = "rss"; public static final String APP_PROPERTY_TX_POWER = "tx_power"; public static final String APP_PROPERTY_CHANNEL = "channel"; @@ -47,15 +49,19 @@ public class LoraConstants { public static final String APP_PROPERTY_BANDWIDTH = "bandwidth"; public static final String APP_PROPERTY_SNR = "snr"; public static final String APP_PROPERTY_FUNCTION_PORT = "function_port"; + public static final String APP_PROPERTY_FUNCTION_ALTITUDE = "altitude"; public static final String APP_PROPERTY_FUNCTION_LATITUDE = "latitude"; public static final String APP_PROPERTY_FUNCTION_LONGITUDE = "longitude"; public static final String APP_PROPERTY_MIC = "mic"; + public static final String ADAPTIVE_DATA_RATE_ENABLED = "adr_enabled"; public static final String GATEWAYS = "gateways"; public static final String GATEWAY_ID = "gateway_id"; public static final String DATA_RATE = "data_rate"; + public static final String DATA_RATE_ID = "data_rate_id"; public static final String CODING_RATE = "coding_rate"; public static final String FREQUENCY = "frequency"; public static final String FRAME_COUNT = "frame_count"; + public static final String LOCATION = "location"; private LoraConstants() { // prevent instantiation diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/LoraMetaData.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/LoraMetaData.java new file mode 100644 index 0000000000..1ed469a2c8 --- /dev/null +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/LoraMetaData.java @@ -0,0 +1,266 @@ +/** + * Copyright (c) 2020 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + + +package org.eclipse.hono.adapter.lora; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A container for meta information contained in Lora + * messages. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class LoraMetaData { + + @JsonProperty(LoraConstants.APP_PROPERTY_BANDWIDTH) + private Integer bandwidth; + @JsonProperty(LoraConstants.APP_PROPERTY_FUNCTION_PORT) + private Integer functionPort; + @JsonProperty(LoraConstants.FRAME_COUNT) + private Integer frameCount; + @JsonProperty(LoraConstants.FREQUENCY) + private Double frequency; + @JsonProperty(LoraConstants.DATA_RATE) + private Integer dataRate; + @JsonProperty(LoraConstants.DATA_RATE_ID) + private String dataRateIdentifier; + @JsonProperty(LoraConstants.CODING_RATE) + private String codingRate; + @JsonProperty(LoraConstants.ADAPTIVE_DATA_RATE_ENABLED) + private Boolean adaptiveDataRateEnabled; + @JsonProperty(LoraConstants.APP_PROPERTY_SPREADING_FACTOR) + private Integer spreadingFactor; + @JsonProperty(LoraConstants.LOCATION) + private Location location; + @JsonProperty(LoraConstants.GATEWAYS) + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List gatewayInfo = new LinkedList<>(); + + /** + * Gets the bandwidth used by the device's radio for sending the data. + * + * @return The bandwidth in kHz or {@code null} if unknown. + * @see + * Data Rate and Spreading Factor + */ + public Integer getBandwidth() { + return bandwidth; + } + + /** + * Sets the bandwidth used by the device's radio for sending the data. + * + * @param bandwidth The bandwidth in kHz or {@code null} if unknown. + * @return This object for command chaining. + * @see + * Data Rate and Spreading Factor + */ + public LoraMetaData setBandwidth(final Integer bandwidth) { + this.bandwidth = bandwidth; + return this; + } + + /** + * Gets the function port number used to represent the type and/or + * characteristics of the payload data. + * + * @return The port number or {@code null} if unknown. + */ + public Integer getFunctionPort() { + return functionPort; + } + + /** + * Sets the function port number used to represent the type and/or + * characteristics of the payload data. + * + * @param functionPort The port number or {@code null} if unknown. + * @return This object for command chaining. + */ + public LoraMetaData setFunctionPort(final Integer functionPort) { + this.functionPort = functionPort; + return this; + } + + /** + * Gets the number of uplink messages that have been sent by the device + * since the beginning of the LoRa network session. + * + * @return The number of messages or {@code null} if unknown. + */ + public Integer getFrameCount() { + return frameCount; + } + + /** + * Sets the number of uplink messages that have been sent by the device + * since the beginning of the LoRa network session. + * + * @param frameCount The number of messages or {@code null} if unknown. + * @return This object for command chaining. + */ + public LoraMetaData setFrameCount(final Integer frameCount) { + this.frameCount = frameCount; + return this; + } + + /** + * Gets the frequency used by the device's radio for sending the data. + * + * @return The frequency in mHz or {@code null} if unknown. + */ + public Double getFrequency() { + return frequency; + } + + /** + * Sets the frequency used by the device's radio for sending the data. + * + * @param frequency The frequency in mHz or {@code null} if unknown. + * + * @return This object for command chaining. + * @throws IllegalArgumentException if frequency is negative. + */ + public LoraMetaData setFrequency(final Double frequency) { + if (frequency < 0) { + throw new IllegalArgumentException("frequency must be positive"); + } + this.frequency = frequency; + return this; + } + + /** + * Gets the coding rate used by the device to send the data. + * + * @return The coding rate or {@code null} if unknown. + * @see Code Rate + */ + public String getCodingRate() { + return codingRate; + } + + /** + * Sets the coding rate used by the device to send the data. + * + * @param codingRate The coding rate or {@code null} if unknown. + * @return This object for command chaining. + */ + public LoraMetaData setCodingRate(final String codingRate) { + this.codingRate = codingRate; + return this; + } + + /** + * Checks if the network server uses Adaptive Data Rate (ADR) control to optimize + * the device's data rate. + * + * @return {@code true} if ADR is in use or {@code null} if unknown. + */ + public Boolean getAdaptiveDataRateEnabled() { + return adaptiveDataRateEnabled; + } + + /** + * Sets whether the network server uses Adaptive Data Rate (ADR) control to optimize + * the device's data rate. + * + * @param flag {@code true} if ADR is in use or {@code null} if unknown. + * @return This object for command chaining. + */ + public LoraMetaData setAdaptiveDataRateEnabled(final Boolean flag) { + this.adaptiveDataRateEnabled = flag; + return this; + } + + /** + * Gets the spreading factor used by the device's radio to send the data. + * + * @return The spreading factor or {@code null} if unknown. + * @see + * Data Rate and Spreading Factor + */ + public Integer getSpreadingFactor() { + return spreadingFactor; + } + + /** + * Sets the spreading factor used by the device's radio to send the data. + * + * @param spreadingFactor The spreading factor or {@code null} if unknown. + * @return This object for command chaining. + * @throws IllegalArgumentException if the spreading factor is smaller than 7 or greater than 12. + * @see + * Data Rate and Spreading Factor + */ + public LoraMetaData setSpreadingFactor(final Integer spreadingFactor) { + + if (spreadingFactor != null) { + if (spreadingFactor < 7 || spreadingFactor > 12) { + throw new IllegalArgumentException("spreading factor must be > 6 and < 13"); + } + } + this.spreadingFactor = spreadingFactor; + return this; + } + + /** + * Gets the location of the device. + * + * @return The location or {@code null} if unknown. + */ + public Location getLocation() { + return location; + } + + /** + * Sets the location of the device. + * + * @param location The location or {@code null} if unknown. + * @return This object for command chaining. + */ + public LoraMetaData setLocation(final Location location) { + this.location = location; + return this; + } + + /** + * Adds meta information for a gateway. + * + * @param gwInfo The meta information. + * @return This object for command chaining. + * @throws NullPointerException if info is {@code null}. + */ + @JsonIgnore + public LoraMetaData addGatewayInfo(final GatewayInfo gwInfo) { + Objects.requireNonNull(gwInfo); + this.gatewayInfo.add(gwInfo); + return this; + } + + /** + * Gets meta information for the gateways. + * + * @return An unmodifiable list of meta information. + */ + public List getGatewayInfo() { + return Collections.unmodifiableList(gatewayInfo); + } +} diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/UplinkLoraMessage.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/UplinkLoraMessage.java index b357cf0bdc..a9a1671edd 100644 --- a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/UplinkLoraMessage.java +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/UplinkLoraMessage.java @@ -14,9 +14,6 @@ package org.eclipse.hono.adapter.lora; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; import com.google.common.io.BaseEncoding; @@ -34,7 +31,7 @@ public class UplinkLoraMessage implements LoraMessage { private final byte[] devEui; private final String devEuiAsString; private Buffer payload; - private Map normalizedData = new HashMap<>(); + private LoraMetaData metaData = null; private JsonObject additionalData; /** @@ -110,26 +107,21 @@ public final void setPayload(final Buffer payload) { } /** - * Gets the normalized data contained in this message. + * Gets the meta data contained in this message. * - * @return The normalized data. + * @return The meta data. */ - public final Map getNormalizedData() { - return Collections.unmodifiableMap(normalizedData); + public final LoraMetaData getMetaData() { + return metaData; } /** - * Sets the normalized data contained in this message. + * Sets the meta data contained in this message. * - * @param data The normalized data. - * @throws NullPointerException if data is {@code null}. + * @param data The meta data. */ - public final void setNormalizedData(final Map data) { - Objects.requireNonNull(data); - synchronized (normalizedData) { - normalizedData.clear(); - normalizedData.putAll(data); - } + public final void setMetaData(final LoraMetaData data) { + this.metaData = data; } /** diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/impl/LoraProtocolAdapter.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/impl/LoraProtocolAdapter.java index ee372fa1bd..f7a41e5a6a 100644 --- a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/impl/LoraProtocolAdapter.java +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/impl/LoraProtocolAdapter.java @@ -24,6 +24,7 @@ import org.eclipse.hono.adapter.lora.LoraConstants; import org.eclipse.hono.adapter.lora.LoraMessage; import org.eclipse.hono.adapter.lora.LoraMessageType; +import org.eclipse.hono.adapter.lora.LoraMetaData; import org.eclipse.hono.adapter.lora.LoraProtocolAdapterProperties; import org.eclipse.hono.adapter.lora.UplinkLoraMessage; import org.eclipse.hono.adapter.lora.providers.LoraProvider; @@ -54,6 +55,8 @@ import io.opentracing.tag.Tag; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpMethod; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.ChainAuthHandler; @@ -154,23 +157,22 @@ private void setupAuthorization(final Router router) { router.route().handler(authHandler); } - @SuppressWarnings("unchecked") @Override protected void customizeDownstreamMessage(final Message downstreamMessage, final RoutingContext ctx) { MessageHelper.addProperty(downstreamMessage, LoraConstants.APP_PROPERTY_ORIG_LORA_PROVIDER, ctx.get(LoraConstants.APP_PROPERTY_ORIG_LORA_PROVIDER)); - Optional.ofNullable(ctx.get(LoraConstants.NORMALIZED_PROPERTIES)) - .filter(Map.class::isInstance) - .map(Map.class::cast) - .ifPresent(properties -> { - ((Map) properties).entrySet() - .forEach(entry -> MessageHelper.addProperty(downstreamMessage, entry.getKey(), entry.getValue())); + Optional.ofNullable(ctx.get(LoraConstants.META_DATA)) + .map(LoraMetaData.class::cast) + .ifPresent(metaData -> { + final String json = Json.encode(metaData); + MessageHelper.addProperty(downstreamMessage, LoraConstants.META_DATA, json); }); Optional.ofNullable(ctx.get(LoraConstants.ADDITIONAL_DATA)) - .ifPresent(data -> MessageHelper.addProperty(downstreamMessage, LoraConstants.ADDITIONAL_DATA, data)); + .map(JsonObject.class::cast) + .ifPresent(data -> MessageHelper.addProperty(downstreamMessage, LoraConstants.ADDITIONAL_DATA, data.encode())); } void handleProviderRoute(final RoutingContext ctx, final LoraProvider provider) { @@ -191,7 +193,7 @@ void handleProviderRoute(final RoutingContext ctx, final LoraProvider provider) TracingHelper.setDeviceTags(currentSpan, gatewayDevice.getTenantId(), gatewayDevice.getDeviceId()); try { - final LoraMessage loraMessage = provider.getMessage(ctx.getBody()); + final LoraMessage loraMessage = provider.getMessage(ctx); final LoraMessageType type = loraMessage.getType(); currentSpan.log(Map.of("message type", type)); final String deviceId = loraMessage.getDevEUIAsString(); @@ -202,8 +204,8 @@ void handleProviderRoute(final RoutingContext ctx, final LoraProvider provider) final UplinkLoraMessage uplinkMessage = (UplinkLoraMessage) loraMessage; final Buffer payload = uplinkMessage.getPayload(); - Optional.ofNullable(uplinkMessage.getNormalizedData()) - .ifPresent(data -> ctx.put(LoraConstants.NORMALIZED_PROPERTIES, data)); + Optional.ofNullable(uplinkMessage.getMetaData()) + .ifPresent(data -> ctx.put(LoraConstants.META_DATA, data)); Optional.ofNullable(uplinkMessage.getAdditionalData()) .ifPresent(data -> ctx.put(LoraConstants.ADDITIONAL_DATA, data)); diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ActilityProvider.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ActilityProvider.java index bafea2c890..11a115ff94 100644 --- a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ActilityProvider.java +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ActilityProvider.java @@ -13,13 +13,12 @@ package org.eclipse.hono.adapter.lora.providers; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; import java.util.Optional; -import org.eclipse.hono.adapter.lora.LoraConstants; +import org.eclipse.hono.adapter.lora.GatewayInfo; import org.eclipse.hono.adapter.lora.LoraMessageType; +import org.eclipse.hono.adapter.lora.LoraMetaData; import org.springframework.stereotype.Component; import com.google.common.io.BaseEncoding; @@ -32,23 +31,23 @@ * A LoRaWAN provider with API for Actility. */ @Component -public class ActilityProvider extends BaseLoraProvider { +public class ActilityProvider extends JsonBasedLoraProvider { - private static final String FIELD_ACTILITY_ROOT_OBJECT = "DevEUI_uplink"; - private static final String FIELD_ACTILITY_DEVICE_EUI = "DevEUI"; - private static final String FIELD_ACTILITY_PAYLOAD = "payload_hex"; - private static final String FIELD_ACTILITY_LRR_RSSI = "LrrRSSI"; - private static final String FIELD_ACTILITY_TX_POWER = "TxPower"; + private static final String FIELD_ACTILITY_ADR = "ADRbit"; private static final String FIELD_ACTILITY_CHANNEL = "Channel"; - private static final String FIELD_ACTILITY_SUB_BAND = "SubBand"; - private static final String FIELD_ACTILITY_SPREADING_FACTOR = "SpFact"; - private static final String FIELD_ACTILITY_LRR_SNR = "LrrSNR"; + private static final String FIELD_ACTILITY_DEVICE_EUI = "DevEUI"; private static final String FIELD_ACTILITY_FPORT = "FPort"; + private static final String FIELD_ACTILITY_FRAME_COUNT_UPLINK = "FCntUp"; private static final String FIELD_ACTILITY_LATITUTDE = "LrrLAT"; private static final String FIELD_ACTILITY_LONGITUDE = "LrrLON"; - private static final String FIELD_ACTILITY_LRRS = "Lrrs"; + private static final String FIELD_ACTILITY_ROOT_OBJECT = "DevEUI_uplink"; + private static final String FIELD_ACTILITY_PAYLOAD = "payload_hex"; private static final String FIELD_ACTILITY_LRR = "Lrr"; private static final String FIELD_ACTILITY_LRR_ID = "Lrrid"; + private static final String FIELD_ACTILITY_LRR_RSSI = "LrrRSSI"; + private static final String FIELD_ACTILITY_LRR_SNR = "LrrSNR"; + private static final String FIELD_ACTILITY_LRRS = "Lrrs"; + private static final String FIELD_ACTILITY_SPREADING_FACTOR = "SpFact"; @Override public String getProviderName() { @@ -65,7 +64,7 @@ private Optional getRootObject(final JsonObject loraMessage) { } @Override - protected String extractDevEui(final JsonObject loraMessage) { + protected String getDevEui(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); return getRootObject(loraMessage) @@ -76,7 +75,7 @@ protected String extractDevEui(final JsonObject loraMessage) { } @Override - protected Buffer extractPayload(final JsonObject loraMessage) { + protected Buffer getPayload(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); return getRootObject(loraMessage) .map(root -> root.getValue(FIELD_ACTILITY_PAYLOAD)) @@ -87,7 +86,7 @@ protected Buffer extractPayload(final JsonObject loraMessage) { } @Override - protected LoraMessageType extractMessageType(final JsonObject loraMessage) { + protected LoraMessageType getMessageType(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); return getRootObject(loraMessage) .map(root -> LoraMessageType.UPLINK) @@ -95,137 +94,90 @@ protected LoraMessageType extractMessageType(final JsonObject loraMessage) { } @Override - protected Map extractNormalizedData(final JsonObject loraMessage) { + protected LoraMetaData getMetaData(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); return getRootObject(loraMessage) - .map(this::getNormalizedData) - .orElse(Map.of()); + .map(this::extractMetaData) + .orElse(null); } - private Map getNormalizedData(final JsonObject rootObject) { - - final Map data = new HashMap<>(); - LoraUtils.addNormalizedValue( - rootObject, - FIELD_ACTILITY_LRR_RSSI, - String.class, - LoraConstants.APP_PROPERTY_RSS, - s -> Math.abs(Double.valueOf(s)), - data); - LoraUtils.addNormalizedValue( - rootObject, - FIELD_ACTILITY_TX_POWER, - Double.class, - LoraConstants.APP_PROPERTY_TX_POWER, - d -> d, - data); - LoraUtils.addNormalizedValue( - rootObject, - FIELD_ACTILITY_CHANNEL, - String.class, - LoraConstants.APP_PROPERTY_CHANNEL, - s -> s, - data); - LoraUtils.addNormalizedValue( - rootObject, - FIELD_ACTILITY_SUB_BAND, - String.class, - LoraConstants.APP_PROPERTY_SUB_BAND, - s -> s, - data); - LoraUtils.addNormalizedValue( - rootObject, - FIELD_ACTILITY_SPREADING_FACTOR, - String.class, - LoraConstants.APP_PROPERTY_SPREADING_FACTOR, - s -> Integer.valueOf(s), - data); - LoraUtils.addNormalizedValue( - rootObject, - FIELD_ACTILITY_LRR_SNR, - String.class, - LoraConstants.APP_PROPERTY_SNR, - s -> Math.abs(Double.valueOf(s)), - data); - LoraUtils.addNormalizedValue( - rootObject, - FIELD_ACTILITY_FPORT, - String.class, - LoraConstants.APP_PROPERTY_FUNCTION_PORT, - s -> Integer.valueOf(s), - data); - LoraUtils.addNormalizedValue( - rootObject, - FIELD_ACTILITY_LATITUTDE, - String.class, - LoraConstants.APP_PROPERTY_FUNCTION_LATITUDE, - s -> Double.valueOf(s), - data); - LoraUtils.addNormalizedValue( - rootObject, - FIELD_ACTILITY_LONGITUDE, - String.class, - LoraConstants.APP_PROPERTY_FUNCTION_LONGITUDE, - s -> Double.valueOf(s), - data); + private LoraMetaData extractMetaData(final JsonObject rootObject) { + + final LoraMetaData data = new LoraMetaData(); + + LoraUtils.getChildObject(rootObject, FIELD_ACTILITY_SPREADING_FACTOR, String.class) + .ifPresent(s -> data.setSpreadingFactor(Integer.valueOf(s))); + LoraUtils.getChildObject(rootObject, FIELD_ACTILITY_FPORT, String.class) + .ifPresent(s -> data.setFunctionPort(Integer.valueOf(s))); + LoraUtils.getChildObject(rootObject, FIELD_ACTILITY_FRAME_COUNT_UPLINK, String.class) + .ifPresent(s -> data.setFrameCount(Integer.valueOf(s))); + LoraUtils.getChildObject(rootObject, FIELD_ACTILITY_ADR, String.class) + .ifPresent(s -> data.setAdaptiveDataRateEnabled(s.equals("1") ? Boolean.TRUE : Boolean.FALSE)); + LoraUtils.getChildObject(rootObject, FIELD_ACTILITY_CHANNEL, String.class) + .map(this::getFrequency) + .ifPresent(data::setFrequency); LoraUtils.getChildObject(rootObject, FIELD_ACTILITY_LRRS, JsonObject.class) .map(lrrs -> lrrs.getValue(FIELD_ACTILITY_LRR)) .filter(JsonArray.class::isInstance) .map(JsonArray.class::cast) .ifPresent(lrrList -> { - final JsonArray normalizedGateways = lrrList.stream() + final Optional gwId = LoraUtils.getChildObject(rootObject, FIELD_ACTILITY_LRR_ID, String.class); + lrrList.stream() .filter(JsonObject.class::isInstance) .map(JsonObject.class::cast) - .map(lrr -> { - final JsonObject normalizedGateway = new JsonObject(); - LoraUtils.getChildObject(lrr, FIELD_ACTILITY_LRR_ID, String.class) - .ifPresent(s -> normalizedGateway.put(LoraConstants.GATEWAY_ID, s)); - LoraUtils.getChildObject(lrr, FIELD_ACTILITY_LRR_RSSI, String.class) - .ifPresent(s -> normalizedGateway.put(LoraConstants.APP_PROPERTY_RSS, Math.abs(Double.valueOf(s)))); - LoraUtils.getChildObject(lrr, FIELD_ACTILITY_LRR_SNR, String.class) - .ifPresent(s -> normalizedGateway.put(LoraConstants.APP_PROPERTY_SNR, Math.abs(Double.valueOf(s)))); - return normalizedGateway; - }) - .collect(() -> new JsonArray(), (array, value) -> array.add(value), (array1, array2) -> array1.addAll(array2)); - data.put(LoraConstants.GATEWAYS, normalizedGateways.toString()); + .map(this::extractGatewayInfo) + .forEach(gateway -> { + Optional.ofNullable(gateway.getGatewayId()) + .ifPresent(s -> gwId.ifPresent(id -> { + if (id.equals(s)) { + Optional.ofNullable(LoraUtils.newLocationFromString( + LoraUtils.getChildObject(rootObject, FIELD_ACTILITY_LONGITUDE, String.class), + LoraUtils.getChildObject(rootObject, FIELD_ACTILITY_LATITUTDE, String.class), + Optional.empty())) + .ifPresent(gateway::setLocation); + } + })); + data.addGatewayInfo(gateway); + }); }); - return data; } - @Override - protected JsonObject extractAdditionalData(final JsonObject loraMessage) { - final JsonObject returnMessage = loraMessage.copy(); - if (returnMessage.containsKey(FIELD_ACTILITY_LRR_RSSI)) { - returnMessage.remove(LoraConstants.APP_PROPERTY_RSS); - } - if (returnMessage.containsKey(FIELD_ACTILITY_TX_POWER)) { - returnMessage.remove(LoraConstants.APP_PROPERTY_TX_POWER); - } - if (returnMessage.containsKey(FIELD_ACTILITY_CHANNEL)) { - returnMessage.remove(LoraConstants.APP_PROPERTY_CHANNEL); - } - if (returnMessage.containsKey(FIELD_ACTILITY_SUB_BAND)) { - returnMessage.remove(LoraConstants.APP_PROPERTY_SUB_BAND); + /** + * Gets the frequency corresponding to the channel ID used by Actility/ThingWork + * as described in section 2.4 of the + * + * Advanced Developer Guide. + * + * @param logicalChannelId The channel ID. + * @return The frequency in MHz or {@code null} if the identifier is unknown. + */ + private Double getFrequency(final String logicalChannelId) { + switch (logicalChannelId) { + case "LC1": + return 868.1; + case "LC2": + return 868.3; + case "LC3": + return 868.5; + case "RX2": + return 869.525; + default: + return null; } - if (returnMessage.containsKey(FIELD_ACTILITY_SPREADING_FACTOR)) { - returnMessage.remove(LoraConstants.APP_PROPERTY_SPREADING_FACTOR); - } - if (returnMessage.containsKey(FIELD_ACTILITY_LRR_SNR)) { - returnMessage.remove(LoraConstants.APP_PROPERTY_SNR); - } - if (returnMessage.containsKey(FIELD_ACTILITY_FPORT)) { - returnMessage.remove(LoraConstants.APP_PROPERTY_FUNCTION_PORT); - } - if (returnMessage.containsKey(FIELD_ACTILITY_LATITUTDE)) { - returnMessage.remove(LoraConstants.APP_PROPERTY_FUNCTION_LATITUDE); - } - if (returnMessage.containsKey(FIELD_ACTILITY_LONGITUDE)) { - returnMessage.remove(LoraConstants.APP_PROPERTY_FUNCTION_LONGITUDE); - } - return null; + } + + private GatewayInfo extractGatewayInfo(final JsonObject lrr) { + final GatewayInfo gateway = new GatewayInfo(); + LoraUtils.getChildObject(lrr, FIELD_ACTILITY_LRR_ID, String.class) + .ifPresent(gateway::setGatewayId); + LoraUtils.getChildObject(lrr, FIELD_ACTILITY_LRR_RSSI, String.class) + .ifPresent(s -> gateway.setRssi(Double.valueOf(s).intValue())); + LoraUtils.getChildObject(lrr, FIELD_ACTILITY_LRR_SNR, String.class) + .ifPresent(s -> gateway.setSnr(Double.valueOf(s))); + return gateway; } } diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/BaseLoraProvider.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/BaseLoraProvider.java deleted file mode 100644 index d9576083f5..0000000000 --- a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/BaseLoraProvider.java +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright (c) 2020 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ - -package org.eclipse.hono.adapter.lora.providers; - -import java.util.Map; -import java.util.Objects; - -import org.eclipse.hono.adapter.lora.LoraMessage; -import org.eclipse.hono.adapter.lora.LoraMessageType; -import org.eclipse.hono.adapter.lora.UplinkLoraMessage; - -import io.vertx.core.buffer.Buffer; -import io.vertx.core.json.DecodeException; -import io.vertx.core.json.JsonObject; - -/** - * A base class for implementing {@link LoraProvider}s. - * - */ -abstract class BaseLoraProvider implements LoraProvider { - - @Override - public LoraMessage getMessage(final Buffer body) { - Objects.requireNonNull(body); - try { - final JsonObject requestBody = body.toJsonObject(); - final LoraMessageType type = extractMessageType(requestBody); - switch (type) { - case UPLINK: - return createUplinkMessage(requestBody); - default: - throw new LoraProviderMalformedPayloadException(String.format("unsupported message type [%s]", type)); - } - } catch (final DecodeException | IllegalArgumentException e) { - throw new LoraProviderMalformedPayloadException("failed to decode request body", e); - } - } - - protected UplinkLoraMessage createUplinkMessage(final JsonObject requestBody) { - final String devEui = extractDevEui(requestBody); - final UplinkLoraMessage message = new UplinkLoraMessage(devEui); - message.setPayload(extractPayload(requestBody)); - message.setNormalizedData(extractNormalizedData(requestBody)); - message.setAdditionalData(extractAdditionalData(requestBody)); - return message; - } - - protected abstract String extractDevEui(JsonObject loraMessage); - protected abstract Buffer extractPayload( JsonObject loraMessage); - protected abstract LoraMessageType extractMessageType(JsonObject loraMessage); - protected Map extractNormalizedData(final JsonObject loraMessage) { - return Map.of(); - }; - protected JsonObject extractAdditionalData(final JsonObject loraMessage) { - return null; - }; -} diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ChirpStackProvider.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ChirpStackProvider.java index 474c207eba..3b4c1cc78b 100644 --- a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ChirpStackProvider.java +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ChirpStackProvider.java @@ -14,12 +14,11 @@ package org.eclipse.hono.adapter.lora.providers; import java.util.Base64; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; -import org.eclipse.hono.adapter.lora.LoraConstants; +import org.eclipse.hono.adapter.lora.GatewayInfo; import org.eclipse.hono.adapter.lora.LoraMessageType; +import org.eclipse.hono.adapter.lora.LoraMetaData; import org.springframework.stereotype.Component; import io.vertx.core.buffer.Buffer; @@ -34,26 +33,28 @@ * Protobuf based JSON format. */ @Component -public class ChirpStackProvider extends BaseLoraProvider { +public class ChirpStackProvider extends JsonBasedLoraProvider { - private static final String FIELD_CHIRPSTACK_PAYLOAD = "data"; - private static final String FIELD_CHIRPSTACK_DEVICE = "devEUI"; - private static final String FIELD_CHIRPSTACK_TX_INFO = "txInfo"; - private static final String FIELD_CHIRPSTACK_SPREADING_FACTOR = "spreadingFactor"; + private static final String FIELD_CHIRPSTACK_ADR = "adr"; + private static final String FIELD_CHIRPSTACK_ALTITUDE = "altitude"; private static final String FIELD_CHIRPSTACK_BANDWIDTH = "bandwidth"; - private static final String FIELD_CHIRPSTACK_FUNCTION_PORT = "fPort"; + private static final String FIELD_CHIRPSTACK_CHANNEL = "channel"; private static final String FIELD_CHIRPSTACK_CODE_RATE = "codeRate"; - private static final String FIELD_CHIRPSTACK_LORA_MODULATION_INFO = "loRaModulationInfo"; - private static final String FIELD_CHIRPSTACK_FREQUENCY = "frequency"; + private static final String FIELD_CHIRPSTACK_DEVICE = "devEUI"; private static final String FIELD_CHIRPSTACK_FRAME_COUNT = "fCnt"; - private static final String FIELD_CHIRPSTACK_RX_INFO = "rxInfo"; + private static final String FIELD_CHIRPSTACK_FREQUENCY = "frequency"; + private static final String FIELD_CHIRPSTACK_FUNCTION_PORT = "fPort"; private static final String FIELD_CHIRPSTACK_GATEWAY_ID = "gatewayID"; - private static final String FIELD_CHIRPSTACK_RSSI = "rssi"; - private static final String FIELD_CHIRPSTACK_LSNR = "loRaSNR"; - private static final String FIELD_CHIRPSTACK_CHANNEL = "channel"; - private static final String FIELD_CHIRPSTACK_LOCATION = "location"; private static final String FIELD_CHIRPSTACK_LATITUDE = "latitude"; + private static final String FIELD_CHIRPSTACK_LOCATION = "location"; private static final String FIELD_CHIRPSTACK_LONGITUDE = "longitude"; + private static final String FIELD_CHIRPSTACK_LORA_MODULATION_INFO = "loRaModulationInfo"; + private static final String FIELD_CHIRPSTACK_LSNR = "loRaSNR"; + private static final String FIELD_CHIRPSTACK_PAYLOAD = "data"; + private static final String FIELD_CHIRPSTACK_RSSI = "rssi"; + private static final String FIELD_CHIRPSTACK_RX_INFO = "rxInfo"; + private static final String FIELD_CHIRPSTACK_SPREADING_FACTOR = "spreadingFactor"; + private static final String FIELD_CHIRPSTACK_TX_INFO = "txInfo"; @Override public String getProviderName() { @@ -67,7 +68,7 @@ public String pathPrefix() { @Override - protected String extractDevEui(final JsonObject loraMessage) { + protected String getDevEui(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); return LoraUtils.getChildObject(loraMessage, FIELD_CHIRPSTACK_DEVICE, String.class) .map(s -> LoraUtils.convertFromBase64ToHex(s)) @@ -75,7 +76,7 @@ protected String extractDevEui(final JsonObject loraMessage) { } @Override - protected Buffer extractPayload(final JsonObject loraMessage) { + protected Buffer getPayload(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); return LoraUtils.getChildObject(loraMessage, FIELD_CHIRPSTACK_PAYLOAD, String.class) .map(s -> Buffer.buffer(Base64.getDecoder().decode(s))) @@ -83,7 +84,7 @@ protected Buffer extractPayload(final JsonObject loraMessage) { } @Override - protected LoraMessageType extractMessageType(final JsonObject loraMessage) { + protected LoraMessageType getMessageType(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); if (loraMessage.containsKey(FIELD_CHIRPSTACK_PAYLOAD)) { @@ -94,103 +95,62 @@ protected LoraMessageType extractMessageType(final JsonObject loraMessage) { } @Override - protected Map extractNormalizedData(final JsonObject loraMessage) { + protected LoraMetaData getMetaData(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); - final Map data = new HashMap<>(); - - LoraUtils.addNormalizedValue( - loraMessage, - FIELD_CHIRPSTACK_FUNCTION_PORT, - Integer.class, - LoraConstants.APP_PROPERTY_FUNCTION_PORT, - n -> n, - data); - LoraUtils.addNormalizedValue( - loraMessage, - FIELD_CHIRPSTACK_FRAME_COUNT, - Integer.class, - LoraConstants.FRAME_COUNT, - n -> n, - data); + final LoraMetaData data = new LoraMetaData(); + + LoraUtils.getChildObject(loraMessage, FIELD_CHIRPSTACK_FUNCTION_PORT, Integer.class) + .ifPresent(data::setFunctionPort); + LoraUtils.getChildObject(loraMessage, FIELD_CHIRPSTACK_FRAME_COUNT, Integer.class) + .ifPresent(data::setFrameCount); + LoraUtils.getChildObject(loraMessage, FIELD_CHIRPSTACK_ADR, Boolean.class) + .ifPresent(data::setAdaptiveDataRateEnabled); LoraUtils.getChildObject(loraMessage, FIELD_CHIRPSTACK_TX_INFO, JsonObject.class) .map(txInfo -> { - LoraUtils.addNormalizedValue( - txInfo, - FIELD_CHIRPSTACK_FREQUENCY, - Integer.class, - LoraConstants.FREQUENCY, - v -> v, - data); + LoraUtils.getChildObject(txInfo, FIELD_CHIRPSTACK_FREQUENCY, Integer.class) + .ifPresent(v -> data.setFrequency(v.doubleValue() / 1_000_000)); return txInfo.getValue(FIELD_CHIRPSTACK_LORA_MODULATION_INFO); }) .filter(JsonObject.class::isInstance) .map(JsonObject.class::cast) .ifPresent(modulationInfo -> { - LoraUtils.addNormalizedValue( - modulationInfo, - FIELD_CHIRPSTACK_SPREADING_FACTOR, - Integer.class, - LoraConstants.APP_PROPERTY_SPREADING_FACTOR, - v -> v, - data); - LoraUtils.addNormalizedValue( - modulationInfo, - FIELD_CHIRPSTACK_BANDWIDTH, - Integer.class, - LoraConstants.APP_PROPERTY_BANDWIDTH, - v -> v, - data); - LoraUtils.addNormalizedValue( - modulationInfo, - FIELD_CHIRPSTACK_CODE_RATE, - String.class, - LoraConstants.CODING_RATE, - v -> v, - data); + LoraUtils.getChildObject(modulationInfo, FIELD_CHIRPSTACK_SPREADING_FACTOR, Integer.class) + .ifPresent(data::setSpreadingFactor); + LoraUtils.getChildObject(modulationInfo, FIELD_CHIRPSTACK_BANDWIDTH, Integer.class) + .ifPresent(data::setBandwidth); + LoraUtils.getChildObject(modulationInfo, FIELD_CHIRPSTACK_CODE_RATE, String.class) + .ifPresent(data::setCodingRate); }); LoraUtils.getChildObject(loraMessage, FIELD_CHIRPSTACK_RX_INFO, JsonArray.class) .ifPresent(rxInfoList -> { - final JsonArray normalizedGateways = rxInfoList.stream() - .filter(JsonObject.class::isInstance) - .map(JsonObject.class::cast) - .map(rxInfo -> { - final JsonObject normalizedGateway = new JsonObject(); - LoraUtils.getChildObject(rxInfo, FIELD_CHIRPSTACK_GATEWAY_ID, String.class) - .ifPresent(v -> normalizedGateway.put(LoraConstants.GATEWAY_ID, LoraUtils.convertFromBase64ToHex(v))); - LoraUtils.getChildObject(rxInfo, FIELD_CHIRPSTACK_RSSI, Integer.class) - .ifPresent(v -> normalizedGateway.put(LoraConstants.APP_PROPERTY_RSS, v)); - LoraUtils.getChildObject(rxInfo, FIELD_CHIRPSTACK_LSNR, Double.class) - .ifPresent(v -> normalizedGateway.put(LoraConstants.APP_PROPERTY_SNR, v)); - LoraUtils.getChildObject(rxInfo, FIELD_CHIRPSTACK_CHANNEL, Integer.class) - .ifPresent(v -> normalizedGateway.put(LoraConstants.APP_PROPERTY_CHANNEL, v)); - - LoraUtils.getChildObject(rxInfo, FIELD_CHIRPSTACK_LOCATION, JsonObject.class) - .ifPresent(loc -> { - LoraUtils.getChildObject(loc, FIELD_CHIRPSTACK_LATITUDE, Double.class) - .ifPresent(v -> normalizedGateway.put(LoraConstants.APP_PROPERTY_FUNCTION_LATITUDE, v)); - LoraUtils.getChildObject(loc, FIELD_CHIRPSTACK_LONGITUDE, Double.class) - .ifPresent(v -> normalizedGateway.put(LoraConstants.APP_PROPERTY_FUNCTION_LONGITUDE, v)); - }); - return normalizedGateway; - }) - .collect(() -> new JsonArray(), (array, value) -> array.add(value), (array1, array2) -> array1.addAll(array2)); - data.put(LoraConstants.GATEWAYS, normalizedGateways.toString()); + rxInfoList.stream() + .filter(JsonObject.class::isInstance) + .map(JsonObject.class::cast) + .forEach(rxInfo -> { + final GatewayInfo gateway = new GatewayInfo(); + LoraUtils.getChildObject(rxInfo, FIELD_CHIRPSTACK_GATEWAY_ID, String.class) + .ifPresent(v -> gateway.setGatewayId(LoraUtils.convertFromBase64ToHex(v))); + LoraUtils.getChildObject(rxInfo, FIELD_CHIRPSTACK_RSSI, Integer.class) + .ifPresent(gateway::setRssi); + LoraUtils.getChildObject(rxInfo, FIELD_CHIRPSTACK_LSNR, Double.class) + .ifPresent(gateway::setSnr); + LoraUtils.getChildObject(rxInfo, FIELD_CHIRPSTACK_CHANNEL, Integer.class) + .ifPresent(gateway::setChannel); + + LoraUtils.getChildObject(rxInfo, FIELD_CHIRPSTACK_LOCATION, JsonObject.class) + .map(loc -> LoraUtils.newLocation( + LoraUtils.getChildObject(loc, FIELD_CHIRPSTACK_LONGITUDE, Double.class), + LoraUtils.getChildObject(loc, FIELD_CHIRPSTACK_LATITUDE, Double.class), + LoraUtils.getChildObject(loc, FIELD_CHIRPSTACK_ALTITUDE, Double.class))) + .ifPresent(gateway::setLocation); + data.addGatewayInfo(gateway); + }); }); return data; } - - /** - * {@inheritDoc} - * - * @return Always {@code null}. - */ - @Override - protected JsonObject extractAdditionalData(final JsonObject loraMessage) { - return null; - } } diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/EverynetProvider.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/EverynetProvider.java index c2f03a12e4..61b3cedc31 100644 --- a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/EverynetProvider.java +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/EverynetProvider.java @@ -17,7 +17,9 @@ import java.util.Objects; import java.util.Optional; +import org.eclipse.hono.adapter.lora.GatewayInfo; import org.eclipse.hono.adapter.lora.LoraMessageType; +import org.eclipse.hono.adapter.lora.LoraMetaData; import org.eclipse.hono.service.http.HttpUtils; import org.springframework.stereotype.Component; @@ -27,16 +29,37 @@ /** * A LoRaWAN provider with API for Everynet. + *

+ * This provider supports uplink messages as described in the + * + * Everynet API */ @Component -public class EverynetProvider extends BaseLoraProvider { +public class EverynetProvider extends JsonBasedLoraProvider { - private static final String FIELD_EVERYNET_ROOT_PARAMS_OBJECT = "params"; - private static final String FIELD_EVERYNET_ROOT_META_OBJECT = "meta"; + private static final String FIELD_EVERYNET_ALTITUDE = "alt"; + private static final String FIELD_EVERYNET_BANDWIDTH = "bandwidth"; + private static final String FIELD_EVERYNET_CHANNEL = "channel"; + private static final String FIELD_EVERYNET_CODERATE = "coderate"; private static final String FIELD_EVERYNET_DEVICE_EUI = "device"; + private static final String FIELD_EVERYNET_FRAME_COUNT = "counter_up"; + private static final String FIELD_EVERYNET_FREQUENCY = "freq"; + private static final String FIELD_EVERYNET_LATITUDE = "lat"; + private static final String FIELD_EVERYNET_LONGITUDE = "lng"; private static final String FIELD_EVERYNET_PAYLOAD = "payload"; + private static final String FIELD_EVERYNET_PORT = "port"; + private static final String FIELD_EVERYNET_RSSI = "rssi"; + private static final String FIELD_EVERYNET_SNR = "snr"; + private static final String FIELD_EVERYNET_SPREADING_FACTOR = "spreading"; private static final String FIELD_EVERYNET_TYPE = "type"; + private static final String OBJECT_EVERYNET_GPS = "gps"; + private static final String OBJECT_EVERYNET_HARDWARE = "hardware"; + private static final String OBJECT_EVERYNET_PARAMS = "params"; + private static final String OBJECT_EVERYNET_META = "meta"; + private static final String OBJECT_EVERYNET_MODULATION = "modulation"; + private static final String OBJECT_EVERYNET_RADIO = "radio"; + @Override public String getProviderName() { return "everynet"; @@ -47,8 +70,12 @@ public String pathPrefix() { return "/everynet"; } - private Optional getRootObject(final JsonObject loraMessage) { - return LoraUtils.getChildObject(loraMessage, FIELD_EVERYNET_ROOT_META_OBJECT, JsonObject.class); + private Optional getMetaObject(final JsonObject loraMessage) { + return LoraUtils.getChildObject(loraMessage, OBJECT_EVERYNET_META, JsonObject.class); + } + + private Optional getParamsObject(final JsonObject loraMessage) { + return LoraUtils.getChildObject(loraMessage, OBJECT_EVERYNET_PARAMS, JsonObject.class); } /** @@ -72,23 +99,23 @@ public HttpMethod acceptedHttpMethod() { } @Override - protected String extractDevEui(final JsonObject loraMessage) { + protected String getDevEui(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); - return getRootObject(loraMessage) - .map(root -> root.getValue(FIELD_EVERYNET_DEVICE_EUI)) + return getMetaObject(loraMessage) + .map(meta -> meta.getValue(FIELD_EVERYNET_DEVICE_EUI)) .filter(String.class::isInstance) .map(String.class::cast) .orElseThrow(() -> new LoraProviderMalformedPayloadException("message does not contain String valued device ID property")); } @Override - protected Buffer extractPayload(final JsonObject loraMessage) { + protected Buffer getPayload(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); - return LoraUtils.getChildObject(loraMessage, FIELD_EVERYNET_ROOT_PARAMS_OBJECT, JsonObject.class) - .map(root -> root.getValue(FIELD_EVERYNET_PAYLOAD)) + return getParamsObject(loraMessage) + .map(params -> params.getValue(FIELD_EVERYNET_PAYLOAD)) .filter(String.class::isInstance) .map(String.class::cast) .map(s -> Buffer.buffer(Base64.getDecoder().decode(s))) @@ -96,10 +123,66 @@ protected Buffer extractPayload(final JsonObject loraMessage) { } @Override - protected LoraMessageType extractMessageType(final JsonObject loraMessage) { + protected LoraMessageType getMessageType(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); return LoraUtils.getChildObject(loraMessage, FIELD_EVERYNET_TYPE, String.class) .map(s -> "uplink".equals(s) ? LoraMessageType.UPLINK : LoraMessageType.UNKNOWN) .orElse(LoraMessageType.UNKNOWN); } + + /** + * {@inheritDoc} + */ + @Override + protected LoraMetaData getMetaData(final JsonObject loraMessage) { + + Objects.requireNonNull(loraMessage); + + final LoraMetaData metaData = new LoraMetaData(); + getParamsObject(loraMessage) + .map(params -> { + LoraUtils.getChildObject(params, FIELD_EVERYNET_PORT, Integer.class) + .ifPresent(metaData::setFunctionPort); + LoraUtils.getChildObject(params, FIELD_EVERYNET_FRAME_COUNT, Integer.class) + .ifPresent(metaData::setFrameCount); + return params.getValue(OBJECT_EVERYNET_RADIO); + }) + .filter(JsonObject.class::isInstance) + .map(JsonObject.class::cast) + .ifPresent(radio -> { + LoraUtils.getChildObject(radio, FIELD_EVERYNET_FREQUENCY, Double.class) + .ifPresent(metaData::setFrequency); + + LoraUtils.getChildObject(radio, OBJECT_EVERYNET_MODULATION, JsonObject.class) + .ifPresent(modulation -> { + LoraUtils.getChildObject(modulation, FIELD_EVERYNET_SPREADING_FACTOR, Integer.class) + .ifPresent(metaData::setSpreadingFactor); + LoraUtils.getChildObject(modulation, FIELD_EVERYNET_BANDWIDTH, Integer.class) + .map(v -> v / 1000) + .ifPresent(metaData::setBandwidth); + LoraUtils.getChildObject(modulation, FIELD_EVERYNET_CODERATE, String.class) + .ifPresent(metaData::setCodingRate); + }); + + LoraUtils.getChildObject(radio, OBJECT_EVERYNET_HARDWARE, JsonObject.class) + .ifPresent(hardware -> { + final GatewayInfo gwInfo = new GatewayInfo(); + LoraUtils.getChildObject(hardware, FIELD_EVERYNET_CHANNEL, Integer.class) + .ifPresent(gwInfo::setChannel); + LoraUtils.getChildObject(hardware, FIELD_EVERYNET_RSSI, Integer.class) + .ifPresent(gwInfo::setRssi); + LoraUtils.getChildObject(hardware, FIELD_EVERYNET_SNR, Double.class) + .ifPresent(gwInfo::setSnr); + LoraUtils.getChildObject(hardware, OBJECT_EVERYNET_GPS, JsonObject.class) + .map(gps -> LoraUtils.newLocation( + LoraUtils.getChildObject(gps, FIELD_EVERYNET_LONGITUDE, Double.class), + LoraUtils.getChildObject(gps, FIELD_EVERYNET_LATITUDE, Double.class), + LoraUtils.getChildObject(gps, FIELD_EVERYNET_ALTITUDE, Double.class))) + .ifPresent(gwInfo::setLocation); + metaData.addGatewayInfo(gwInfo); + }); + }); + + return metaData; + } } diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/FireflyProvider.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/FireflyProvider.java index 055cb36c80..aaa4f34387 100644 --- a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/FireflyProvider.java +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/FireflyProvider.java @@ -13,12 +13,11 @@ package org.eclipse.hono.adapter.lora.providers; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; -import org.eclipse.hono.adapter.lora.LoraConstants; +import org.eclipse.hono.adapter.lora.GatewayInfo; import org.eclipse.hono.adapter.lora.LoraMessageType; +import org.eclipse.hono.adapter.lora.LoraMetaData; import org.springframework.stereotype.Component; import com.google.common.io.BaseEncoding; @@ -29,30 +28,33 @@ /** * A LoRaWAN provider with API for Firefly. + *

+ * This provider supports messages as described by the + * Firefly API. */ @Component -public class FireflyProvider extends BaseLoraProvider { +public class FireflyProvider extends JsonBasedLoraProvider { - private static final String FIELD_FIREFLY_SPREADING_FACTOR = "spreading_factor"; - private static final String FIELD_FIREFLY_FUNCTION_PORT = "port"; - private static final String FIELD_FIREFLY_PAYLOAD = "payload"; - private static final String FIELD_FIREFLY_SERVER_DATA = "server_data"; - private static final String FIELD_FIREFLY_MESSAGE_TYPE = "mtype"; - private static final String FIELD_FIREFLY_MESSAGE_TYPE_UPLINK = "confirmed_data_up"; - - private static final String FIELD_FIREFLY_DEVICE = "device"; + private static final String FIELD_FIREFLY_ADR = "adr"; + private static final String FIELD_FIREFLY_BANDWIDTH = "bandwidth"; + private static final String FIELD_FIREFLY_CODING_RATE = "codr"; private static final String FIELD_FIREFLY_DEVICE_EUI = "eui"; - private static final String FIELD_FIREFLY_GATEWAY_RX = "gwrx"; + private static final String FIELD_FIREFLY_FRAME_COUNT = "frame_counter"; + private static final String FIELD_FIREFLY_FREQUENCY = "freq"; + private static final String FIELD_FIREFLY_FUNCTION_PORT = "port"; private static final String FIELD_FIREFLY_GATEWAY_EUI = "gweui"; - private static final String FIELD_FIREFLY_RSSI = "rssi"; private static final String FIELD_FIREFLY_LSNR = "lsnr"; - private static final String FIELD_FIREFLY_DATA_RATE = "datr"; - private static final String FIELD_FIREFLY_CODING_RATE = "codr"; - private static final String FIELD_FIREFLY_FREQUENCY = "freq"; - private static final String FIELD_FIREFLY_PARSED_PACKET = "parsed_packet"; - private static final String FIELD_FIREFLY_FRAME_COUNT = "fcnt"; - private static final String FIELD_FIREFLY_BANDWIDTH = "bandwidth"; - private static final String FIELD_FIREFLY_MIC_PASS = "mic_pass"; + private static final String FIELD_FIREFLY_MESSAGE_TYPE = "mtype"; + private static final String FIELD_FIREFLY_PAYLOAD = "payload"; + private static final String FIELD_FIREFLY_RSSI = "rssi"; + private static final String FIELD_FIREFLY_SPREADING_FACTOR = "spreading_factor"; + + private static final String MESSAGE_TYPE_UPLINK = "confirmed_data_up"; + + private static final String OBJECT_FIREFLY_DEVICE = "device"; + private static final String OBJECT_FIREFLY_GATEWAY_RX = "gwrx"; + private static final String OBJECT_FIREFLY_PARSED_PACKET = "parsed_packet"; + private static final String OBJECT_FIREFLY_SERVER_DATA = "server_data"; @Override public String getProviderName() { @@ -65,10 +67,10 @@ public String pathPrefix() { } @Override - protected String extractDevEui(final JsonObject loraMessage) { + protected String getDevEui(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); - return LoraUtils.getChildObject(loraMessage, FIELD_FIREFLY_DEVICE, JsonObject.class) + return LoraUtils.getChildObject(loraMessage, OBJECT_FIREFLY_DEVICE, JsonObject.class) .map(device -> device.getValue(FIELD_FIREFLY_DEVICE_EUI)) .filter(String.class::isInstance) .map(String.class::cast) @@ -76,7 +78,7 @@ protected String extractDevEui(final JsonObject loraMessage) { } @Override - protected Buffer extractPayload(final JsonObject loraMessage) { + protected Buffer getPayload(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); return LoraUtils.getChildObject(loraMessage, FIELD_FIREFLY_PAYLOAD, String.class) @@ -85,121 +87,67 @@ protected Buffer extractPayload(final JsonObject loraMessage) { } @Override - protected LoraMessageType extractMessageType(final JsonObject loraMessage) { + protected LoraMessageType getMessageType(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); - return LoraUtils.getChildObject(loraMessage, FIELD_FIREFLY_SERVER_DATA, JsonObject.class) + return LoraUtils.getChildObject(loraMessage, OBJECT_FIREFLY_SERVER_DATA, JsonObject.class) .map(serverData -> serverData.getValue(FIELD_FIREFLY_MESSAGE_TYPE)) .filter(String.class::isInstance) .map(String.class::cast) - .map(type -> FIELD_FIREFLY_MESSAGE_TYPE_UPLINK.equals(type) ? LoraMessageType.UPLINK : LoraMessageType.UNKNOWN) + .map(type -> MESSAGE_TYPE_UPLINK.equals(type) ? LoraMessageType.UPLINK : LoraMessageType.UNKNOWN) .orElse(LoraMessageType.UNKNOWN); } @Override - protected Map extractNormalizedData(final JsonObject loraMessage) { + protected LoraMetaData getMetaData(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); - final Map data = new HashMap<>(); - - LoraUtils.addNormalizedValue( - loraMessage, - FIELD_FIREFLY_SPREADING_FACTOR, - Integer.class, - LoraConstants.APP_PROPERTY_SPREADING_FACTOR, - v -> v, - data); - LoraUtils.addNormalizedValue( - loraMessage, - FIELD_FIREFLY_BANDWIDTH, - Integer.class, - LoraConstants.APP_PROPERTY_BANDWIDTH, - v -> v, - data); - LoraUtils.addNormalizedValue( - loraMessage, - FIELD_FIREFLY_FUNCTION_PORT, - Integer.class, - LoraConstants.APP_PROPERTY_FUNCTION_PORT, - v -> v, - data); - LoraUtils.addNormalizedValue( - loraMessage, - FIELD_FIREFLY_MIC_PASS, - Boolean.class, - LoraConstants.APP_PROPERTY_MIC, - v -> v, - data); - - LoraUtils.getChildObject(loraMessage, FIELD_FIREFLY_PARSED_PACKET, JsonObject.class) - .map(parsedPacket -> parsedPacket.getValue(FIELD_FIREFLY_FRAME_COUNT)) - .filter(Integer.class::isInstance) - .map(Integer.class::cast) - .ifPresent(v -> data.put(LoraConstants.FRAME_COUNT, v)); - - - LoraUtils.getChildObject(loraMessage, FIELD_FIREFLY_SERVER_DATA, JsonObject.class) + + final LoraMetaData data = new LoraMetaData(); + + LoraUtils.getChildObject(loraMessage, FIELD_FIREFLY_BANDWIDTH, Integer.class) + .ifPresent(data::setBandwidth); + LoraUtils.getChildObject(loraMessage, FIELD_FIREFLY_SPREADING_FACTOR, Integer.class) + .ifPresent(data::setSpreadingFactor); + LoraUtils.getChildObject(loraMessage, FIELD_FIREFLY_FUNCTION_PORT, Integer.class) + .ifPresent(data::setFunctionPort); + LoraUtils.getChildObject(loraMessage, FIELD_FIREFLY_FRAME_COUNT, Integer.class) + .ifPresent(data::setFrameCount); + + LoraUtils.getChildObject(loraMessage, OBJECT_FIREFLY_PARSED_PACKET, JsonObject.class) + .map(packet -> packet.getValue(FIELD_FIREFLY_ADR)) + .filter(Boolean.class::isInstance) + .map(Boolean.class::cast) + .ifPresent(data::setAdaptiveDataRateEnabled); + + LoraUtils.getChildObject(loraMessage, OBJECT_FIREFLY_SERVER_DATA, JsonObject.class) .map(serverData -> { - LoraUtils.addNormalizedValue( - serverData, - FIELD_FIREFLY_DATA_RATE, - String.class, - LoraConstants.DATA_RATE, - v -> v, - data); - LoraUtils.addNormalizedValue( - serverData, - FIELD_FIREFLY_CODING_RATE, - String.class, - LoraConstants.CODING_RATE, - v -> v, - data); - LoraUtils.addNormalizedValue( - serverData, - FIELD_FIREFLY_FREQUENCY, - Double.class, - LoraConstants.FREQUENCY, - v -> v, - data); - - return serverData.getValue(FIELD_FIREFLY_GATEWAY_RX); + LoraUtils.getChildObject(serverData, FIELD_FIREFLY_FREQUENCY, Double.class) + .ifPresent(data::setFrequency); + LoraUtils.getChildObject(serverData, FIELD_FIREFLY_CODING_RATE, String.class) + .ifPresent(data::setCodingRate); + + return serverData.getValue(OBJECT_FIREFLY_GATEWAY_RX); }) .filter(JsonArray.class::isInstance) .map(JsonArray.class::cast) .ifPresent(gwRxs -> { - final JsonArray normalizedGatways = gwRxs.stream() - .filter(JsonObject.class::isInstance) - .map(JsonObject.class::cast) - .map(gwRx -> { - final JsonObject normalizedGatway = new JsonObject(); - LoraUtils.getChildObject(gwRx, FIELD_FIREFLY_GATEWAY_EUI, String.class) - .ifPresent(v -> normalizedGatway.put(LoraConstants.GATEWAY_ID, v)); - LoraUtils.getChildObject(gwRx, FIELD_FIREFLY_RSSI, Integer.class) - .ifPresent(v -> normalizedGatway.put(LoraConstants.APP_PROPERTY_RSS, v)); - LoraUtils.getChildObject(gwRx, FIELD_FIREFLY_LSNR, Double.class) - .ifPresent(v -> normalizedGatway.put(LoraConstants.APP_PROPERTY_SNR, v)); - return normalizedGatway; - }) - .collect(JsonArray::new, JsonArray::add, JsonArray::addAll); - data.put(LoraConstants.GATEWAYS, normalizedGatways.toString()); + gwRxs.stream() + .filter(JsonObject.class::isInstance) + .map(JsonObject.class::cast) + .forEach(gwRx -> { + final GatewayInfo gwInfo = new GatewayInfo(); + LoraUtils.getChildObject(gwRx, FIELD_FIREFLY_GATEWAY_EUI, String.class) + .ifPresent(gwInfo::setGatewayId); + LoraUtils.getChildObject(gwRx, FIELD_FIREFLY_RSSI, Integer.class) + .ifPresent(gwInfo::setRssi); + LoraUtils.getChildObject(gwRx, FIELD_FIREFLY_LSNR, Double.class) + .ifPresent(gwInfo::setSnr); + data.addGatewayInfo(gwInfo); + }); }); return data; } - - @Override - protected JsonObject extractAdditionalData(final JsonObject loraMessage) { - - Objects.requireNonNull(loraMessage); - - final JsonObject returnMessage = loraMessage.copy(); - if (returnMessage.containsKey(FIELD_FIREFLY_SPREADING_FACTOR)) { - returnMessage.remove(LoraConstants.APP_PROPERTY_SPREADING_FACTOR); - } - if (returnMessage.containsKey(FIELD_FIREFLY_FUNCTION_PORT)) { - returnMessage.remove(LoraConstants.APP_PROPERTY_FUNCTION_PORT); - } - return null; - } } diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/JsonBasedLoraProvider.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/JsonBasedLoraProvider.java new file mode 100644 index 0000000000..80793fa426 --- /dev/null +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/JsonBasedLoraProvider.java @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2020 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.eclipse.hono.adapter.lora.providers; + +import java.util.Objects; + +import org.eclipse.hono.adapter.lora.LoraMessage; +import org.eclipse.hono.adapter.lora.LoraMessageType; +import org.eclipse.hono.adapter.lora.LoraMetaData; +import org.eclipse.hono.adapter.lora.UplinkLoraMessage; + +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; + +/** + * A base class for implementing {@link LoraProvider}s + * that are using JSON messages in their external API. + * + */ +abstract class JsonBasedLoraProvider implements LoraProvider { + + @Override + public LoraMessage getMessage(final RoutingContext ctx) { + Objects.requireNonNull(ctx); + try { + final Buffer requestBody = ctx.getBody(); + final JsonObject message = requestBody.toJsonObject(); + final LoraMessageType type = getMessageType(message); + switch (type) { + case UPLINK: + return createUplinkMessage(ctx.request(), message); + default: + throw new LoraProviderMalformedPayloadException(String.format("unsupported message type [%s]", type)); + } + } catch (final RuntimeException e) { + // catch generic exception in order to also cover any (runtime) exceptions + // thrown by overridden methods + throw new LoraProviderMalformedPayloadException("failed to decode request body", e); + } + } + + /** + * Gets the type of a Lora message. + * + * @param loraMessage The message. + * @return The type. + */ + protected abstract LoraMessageType getMessageType(JsonObject loraMessage); + + /** + * Gets the device EUI from an uplink message. + * + * @param uplinkMessage The message. + * @return The device EUI. + * @throws RuntimeException if the EUI cannot be extracted. + */ + protected abstract String getDevEui(JsonObject uplinkMessage); + + /** + * Gets the payload from an uplink message. + * + * @param uplinkMessage The message. + * @return The raw bytes sent by the device. + * @throws RuntimeException if the EUI cannot be extracted. + */ + protected abstract Buffer getPayload(JsonObject uplinkMessage); + + /** + * Gets meta data contained in an uplink message. + *

+ * This default implementation returns {@code null}. + * + * @param uplinkMessage The uplink message. + * @return The meta data or {@code null} if no meta data is available. + * @throws RuntimeException if the meta data cannot be parsed. + */ + protected LoraMetaData getMetaData(final JsonObject uplinkMessage) { + return null; + }; + + /** + * Gets any data contained in an uplink message in addition to the device EUI, + * payload and meta data. + *

+ * This default implementation returns the message itself. + * + * @param uplinkMessage The uplink message. + * @return The additional data or {@code null} if no additional data is available. + * @throws RuntimeException if the additional data cannot be parsed. + */ + protected JsonObject getAdditionalData(final JsonObject uplinkMessage) { + return uplinkMessage; + }; + + /** + * Creates an object representation of a Lora uplink message. + *

+ * This method uses the {@link #getDevEui(JsonObject)}, {@link #getPayload(JsonObject)}, + * {@link #getMetaData(JsonObject)} and {@link #getAdditionalData(JsonObject)} + * methods to extract relevant information from the request body to add + * to the returned message. + * + * @param request The request sent by the provider's Network Server. + * @param requestBody The JSON object contained in the request's body. + * @return The message. + * @throws RuntimeException if the message cannot be parsed. + */ + protected UplinkLoraMessage createUplinkMessage(final HttpServerRequest request, final JsonObject requestBody) { + + Objects.requireNonNull(requestBody); + + final String devEui = getDevEui(requestBody); + final UplinkLoraMessage message = new UplinkLoraMessage(devEui); + message.setPayload(getPayload(requestBody)); + message.setMetaData(getMetaData(requestBody)); + message.setAdditionalData(getAdditionalData(requestBody)); + return message; + } +} diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/KerlinkProvider.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/KerlinkProvider.java index 4a8719ce7a..47af5bee21 100644 --- a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/KerlinkProvider.java +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/KerlinkProvider.java @@ -26,7 +26,7 @@ * A LoRaWAN provider with API for Kerlink. */ @Component -public class KerlinkProvider extends BaseLoraProvider { +public class KerlinkProvider extends JsonBasedLoraProvider { static final String FIELD_KERLINK_CLUSTER_ID = "cluster-id"; static final String FIELD_KERLINK_CUSTOMER_ID = "customer-id"; @@ -53,7 +53,7 @@ public String pathPrefix() { * This method always returns {@link LoraMessageType#UPLINK}. */ @Override - public LoraMessageType extractMessageType(final JsonObject loraMessage) { + public LoraMessageType getMessageType(final JsonObject loraMessage) { return LoraMessageType.UPLINK; } @@ -68,7 +68,7 @@ public String acceptedContentType() { } @Override - protected String extractDevEui(final JsonObject loraMessage) { + protected String getDevEui(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); return LoraUtils.getChildObject(loraMessage, FIELD_UPLINK_DEVICE_EUI, String.class) @@ -76,7 +76,7 @@ protected String extractDevEui(final JsonObject loraMessage) { } @Override - protected Buffer extractPayload(final JsonObject loraMessage) { + protected Buffer getPayload(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); return LoraUtils.getChildObject(loraMessage, FIELD_UPLINK_USER_DATA, JsonObject.class) diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/LoraProvider.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/LoraProvider.java index b90923216d..dff095b038 100644 --- a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/LoraProvider.java +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/LoraProvider.java @@ -16,8 +16,8 @@ import org.eclipse.hono.adapter.lora.LoraMessage; import org.eclipse.hono.service.http.HttpUtils; -import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.RoutingContext; /** * A LoraWAN provider which can send and receive messages from and to LoRa devices. @@ -61,10 +61,10 @@ default HttpMethod acceptedHttpMethod() { * Gets the object representation of a message payload sent by * the provider. * - * @param body The raw request body. + * @param request The HTTP request containing the message. * @return The request object. * @throws NullPointerException if body is {@code null}. - * @throws LoraProviderMalformedPayloadException if the body cannot be decoded. + * @throws LoraProviderMalformedPayloadException if the request body cannot be decoded. */ - LoraMessage getMessage(Buffer body); + LoraMessage getMessage(RoutingContext request); } diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/LoraUtils.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/LoraUtils.java index 3549fe7646..adeecb1d37 100644 --- a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/LoraUtils.java +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/LoraUtils.java @@ -19,6 +19,7 @@ import java.util.Optional; import java.util.function.Function; +import org.eclipse.hono.adapter.lora.Location; import org.eclipse.hono.adapter.lora.LoraConstants; import org.eclipse.hono.util.RegistrationConstants; @@ -71,6 +72,58 @@ public static Optional getChildObject(final JsonObject parent, final Stri .map(expectedType::cast); } + /** + * Creates a new location for coordinates. + * + * @param longitude The longitude. + * @param latitude The latitude. + * @param altitude The altitude. + * @return The location or {@code null} if longitude or latitude cannot be parsed into a double. + * @throws NullPointerException if any of the parameters are {@code null}. + */ + public static Location newLocationFromString(final Optional longitude, final Optional latitude, final Optional altitude) { + + Objects.requireNonNull(longitude); + Objects.requireNonNull(latitude); + Objects.requireNonNull(altitude); + + if (longitude.isEmpty() || latitude.isEmpty()) { + return null; + } else { + try { + final Double lon = Double.valueOf(longitude.get()); + final Double lat = Double.valueOf(latitude.get()); + final Double alt = altitude.map(Double::valueOf).orElse(null); + return new Location(lon, lat, alt); + } catch (final NumberFormatException e) { + return null; + } + } + } + + /** + * Creates a new location for coordinates. + * + * @param longitude The longitude. + * @param latitude The latitude. + * @param altitude The altitude. + * @return The location or {@code null} if longitude or latitude are empty. + * @throws NullPointerException if any of the parameters are {@code null}. + */ + public static Location newLocation(final Optional longitude, final Optional latitude, final Optional altitude) { + + Objects.requireNonNull(longitude); + Objects.requireNonNull(latitude); + Objects.requireNonNull(altitude); + + if (longitude.isEmpty() || latitude.isEmpty()) { + return null; + } else { + return new Location(longitude.get(), latitude.get(), altitude.orElse(null)); + } + } + + /** * Extracts a property from a JSON object, applies a normalization function to * it and adds it to a map. diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/LoriotProvider.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/LoriotProvider.java index e4ba177ef9..06136bf242 100644 --- a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/LoriotProvider.java +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/LoriotProvider.java @@ -13,14 +13,14 @@ package org.eclipse.hono.adapter.lora.providers; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.eclipse.hono.adapter.lora.LoraConstants; +import org.eclipse.hono.adapter.lora.GatewayInfo; import org.eclipse.hono.adapter.lora.LoraMessageType; +import org.eclipse.hono.adapter.lora.LoraMetaData; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @@ -38,24 +38,28 @@ * Gateway Information messages. */ @Component -public class LoriotProvider extends BaseLoraProvider { +public class LoriotProvider extends JsonBasedLoraProvider { private static final Logger LOG = LoggerFactory.getLogger(LoriotProvider.class); private static final Pattern PATTERN_DATA_RATE = Pattern.compile("^SF(\\d+) BW(\\d+) (.+)$"); - private static final String FIELD_LORIOT_EUI = "EUI"; - private static final String FIELD_LORIOT_PAYLOAD = "data"; - private static final String FIELD_LORIOT_MESSAGE_TYPE_UPLINK = "gw"; - private static final String FIELD_LORIOT_MESSAGE_TYPE = "cmd"; private static final String FIELD_LORIOT_DATARATE = "dr"; - private static final String FIELD_LORIOT_FUNCTION_PORT = "port"; + private static final String FIELD_LORIOT_EUI = "EUI"; private static final String FIELD_LORIOT_FRAME_COUNT = "fcnt"; private static final String FIELD_LORIOT_FREQUENCY = "freq"; - private static final String FIELD_LORIOT_GATEWAYS = "gws"; + private static final String FIELD_LORIOT_FUNCTION_PORT = "port"; private static final String FIELD_LORIOT_GATEWAY_EUI = "gweui"; + private static final String FIELD_LORIOT_LATITUDE = "lat"; + private static final String FIELD_LORIOT_LONGITUDE = "lon"; + private static final String FIELD_LORIOT_MESSAGE_TYPE = "cmd"; + private static final String FIELD_LORIOT_PAYLOAD = "data"; private static final String FIELD_LORIOT_RSSI = "rssi"; private static final String FIELD_LORIOT_SNR = "snr"; + private static final String OBJECTS_LORIOT_GATEWAYS = "gws"; + + private static final String MESSAGE_TYPE_UPLINK = "gw"; + @Override public String getProviderName() { return "loriot"; @@ -67,7 +71,7 @@ public String pathPrefix() { } @Override - protected String extractDevEui(final JsonObject loraMessage) { + protected String getDevEui(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); return LoraUtils.getChildObject(loraMessage, FIELD_LORIOT_EUI, String.class) @@ -75,7 +79,7 @@ protected String extractDevEui(final JsonObject loraMessage) { } @Override - protected Buffer extractPayload(final JsonObject loraMessage) { + protected Buffer getPayload(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); return LoraUtils.getChildObject(loraMessage, FIELD_LORIOT_PAYLOAD, String.class) @@ -84,83 +88,63 @@ protected Buffer extractPayload(final JsonObject loraMessage) { } @Override - protected LoraMessageType extractMessageType(final JsonObject loraMessage) { + protected LoraMessageType getMessageType(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); return LoraUtils.getChildObject(loraMessage, FIELD_LORIOT_MESSAGE_TYPE, String.class) - .map(s -> FIELD_LORIOT_MESSAGE_TYPE_UPLINK.equals(s) ? LoraMessageType.UPLINK : LoraMessageType.UNKNOWN) + .map(s -> MESSAGE_TYPE_UPLINK.equals(s) ? LoraMessageType.UPLINK : LoraMessageType.UNKNOWN) .orElse(LoraMessageType.UNKNOWN); } @Override - protected Map extractNormalizedData(final JsonObject loraMessage) { + protected LoraMetaData getMetaData(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); - final Map data = new HashMap<>(); - - LoraUtils.addNormalizedValue( - loraMessage, - FIELD_LORIOT_FUNCTION_PORT, - Integer.class, - LoraConstants.APP_PROPERTY_FUNCTION_PORT, - v -> v, - data); - LoraUtils.addNormalizedValue( - loraMessage, - FIELD_LORIOT_FRAME_COUNT, - Integer.class, - LoraConstants.FRAME_COUNT, - v -> v, - data); - LoraUtils.addNormalizedValue( - loraMessage, - FIELD_LORIOT_FREQUENCY, - Double.class, - LoraConstants.FREQUENCY, - v -> v, - data); + final LoraMetaData data = new LoraMetaData(); + + LoraUtils.getChildObject(loraMessage, FIELD_LORIOT_FUNCTION_PORT, Integer.class) + .ifPresent(data::setFunctionPort); + LoraUtils.getChildObject(loraMessage, FIELD_LORIOT_FRAME_COUNT, Integer.class) + .ifPresent(data::setFrameCount); + LoraUtils.getChildObject(loraMessage, FIELD_LORIOT_FREQUENCY, Double.class) + .map(f -> f / 1_000_000) + .ifPresent(data::setFrequency); LoraUtils.getChildObject(loraMessage, FIELD_LORIOT_DATARATE, String.class) .ifPresent(dataRate -> { - data.put(LoraConstants.DATA_RATE, dataRate); final Matcher matcher = PATTERN_DATA_RATE.matcher(dataRate); if (matcher.matches()) { - data.put(LoraConstants.APP_PROPERTY_SPREADING_FACTOR, Integer.parseInt(matcher.group(1))); - data.put(LoraConstants.APP_PROPERTY_BANDWIDTH, Integer.parseInt(matcher.group(2))); - data.put(LoraConstants.CODING_RATE, matcher.group(3)); + data.setSpreadingFactor(Integer.parseInt(matcher.group(1))); + data.setBandwidth(Integer.parseInt(matcher.group(2))); + data.setCodingRate(matcher.group(3)); } else { LOG.debug("invalid data rate [{}]", dataRate); } }); - LoraUtils.getChildObject(loraMessage, FIELD_LORIOT_GATEWAYS, JsonObject.class) - .map(gateways -> gateways.getValue(FIELD_LORIOT_GATEWAYS)) - .filter(JsonArray.class::isInstance) - .map(JsonArray.class::cast) + LoraUtils.getChildObject(loraMessage, OBJECTS_LORIOT_GATEWAYS, JsonArray.class) .ifPresent(gws -> { - final JsonArray normalizedGatways = gws.stream() - .filter(JsonObject.class::isInstance) - .map(JsonObject.class::cast) - .map(gw -> { - final JsonObject normalizedGatway = new JsonObject(); - LoraUtils.getChildObject(gw, FIELD_LORIOT_GATEWAY_EUI, String.class) - .ifPresent(v -> normalizedGatway.put(LoraConstants.GATEWAY_ID, v)); - LoraUtils.getChildObject(gw, FIELD_LORIOT_RSSI, Integer.class) - .ifPresent(v -> normalizedGatway.put(LoraConstants.APP_PROPERTY_RSS, v)); - LoraUtils.getChildObject(gw, FIELD_LORIOT_SNR, Double.class) - .ifPresent(v -> normalizedGatway.put(LoraConstants.APP_PROPERTY_SNR, v)); - return normalizedGatway; - }) - .collect(JsonArray::new, JsonArray::add, JsonArray::addAll); - data.put(LoraConstants.GATEWAYS, normalizedGatways.toString()); + gws.stream() + .filter(JsonObject.class::isInstance) + .map(JsonObject.class::cast) + .forEach(gw -> { + final GatewayInfo gwInfo = new GatewayInfo(); + LoraUtils.getChildObject(gw, FIELD_LORIOT_GATEWAY_EUI, String.class) + .ifPresent(gwInfo::setGatewayId); + LoraUtils.getChildObject(gw, FIELD_LORIOT_RSSI, Integer.class) + .ifPresent(gwInfo::setRssi); + LoraUtils.getChildObject(gw, FIELD_LORIOT_SNR, Double.class) + .ifPresent(gwInfo::setSnr); + Optional.ofNullable(LoraUtils.newLocation( + LoraUtils.getChildObject(gw, FIELD_LORIOT_LONGITUDE, Double.class), + LoraUtils.getChildObject(gw, FIELD_LORIOT_LATITUDE, Double.class), + Optional.empty())) + .ifPresent(gwInfo::setLocation); + data.addGatewayInfo(gwInfo); + }); }); return data; } - - @Override - protected JsonObject extractAdditionalData(final JsonObject loraMessage) { - return null; - } } diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ObjeniousProvider.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ObjeniousProvider.java index b9c9e3042f..59b9a8c5b3 100644 --- a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ObjeniousProvider.java +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ObjeniousProvider.java @@ -14,8 +14,11 @@ package org.eclipse.hono.adapter.lora.providers; import java.util.Objects; +import java.util.Optional; +import org.eclipse.hono.adapter.lora.GatewayInfo; import org.eclipse.hono.adapter.lora.LoraMessageType; +import org.eclipse.hono.adapter.lora.LoraMetaData; import org.springframework.stereotype.Component; import com.google.common.io.BaseEncoding; @@ -31,13 +34,22 @@ * uplink messages only. */ @Component -public class ObjeniousProvider extends BaseLoraProvider { +public class ObjeniousProvider extends JsonBasedLoraProvider { - private static final String FIELD_DEVICE_PROPERTIES = "device_properties"; private static final String FIELD_DEVICE_ID = "deveui"; + private static final String FIELD_FRAME_COUNT = "count"; + private static final String FIELD_LATITUDE = "lat"; + private static final String FIELD_LONGITUDE = "lng"; private static final String FIELD_PAYLOAD = "payload_cleartext"; + private static final String FIELD_PORT = "port"; + private static final String FIELD_RSSI = "rssi"; + private static final String FIELD_SNR = "snr"; + private static final String FIELD_SPREADING_FACTOR = "sf"; private static final String FIELD_TYPE = "type"; + private static final String OBJECT_DEVICE_PROPERTIES = "device_properties"; + private static final String OBJECT_PROTOCOL_DATA = "protocol_data"; + @Override public String getProviderName() { return "objenious"; @@ -49,10 +61,10 @@ public String pathPrefix() { } @Override - protected String extractDevEui(final JsonObject loraMessage) { + protected String getDevEui(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); - return LoraUtils.getChildObject(loraMessage, FIELD_DEVICE_PROPERTIES, JsonObject.class) + return LoraUtils.getChildObject(loraMessage, OBJECT_DEVICE_PROPERTIES, JsonObject.class) .map(props -> props.getValue(FIELD_DEVICE_ID)) .filter(String.class::isInstance) .map(String.class::cast) @@ -60,7 +72,7 @@ protected String extractDevEui(final JsonObject loraMessage) { } @Override - protected Buffer extractPayload(final JsonObject loraMessage) { + protected Buffer getPayload(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); return LoraUtils.getChildObject(loraMessage, FIELD_PAYLOAD, String.class) @@ -69,7 +81,7 @@ protected Buffer extractPayload(final JsonObject loraMessage) { } @Override - protected LoraMessageType extractMessageType(final JsonObject loraMessage) { + protected LoraMessageType getMessageType(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); return LoraUtils.getChildObject(loraMessage, FIELD_TYPE, String.class) @@ -87,4 +99,40 @@ protected LoraMessageType extractMessageType(final JsonObject loraMessage) { }) .orElse(LoraMessageType.UNKNOWN); } + + /** + * {@inheritDoc} + */ + @Override + protected LoraMetaData getMetaData(final JsonObject loraMessage) { + + Objects.requireNonNull(loraMessage); + + final LoraMetaData data = new LoraMetaData(); + + LoraUtils.getChildObject(loraMessage, FIELD_FRAME_COUNT, Integer.class) + .ifPresent(data::setFrameCount); + Optional.ofNullable(LoraUtils.newLocation( + LoraUtils.getChildObject(loraMessage, FIELD_LONGITUDE, Double.class), + LoraUtils.getChildObject(loraMessage, FIELD_LATITUDE, Double.class), + Optional.empty())) + .ifPresent(data::setLocation); + + LoraUtils.getChildObject(loraMessage, OBJECT_PROTOCOL_DATA, JsonObject.class) + .map(prot -> { + LoraUtils.getChildObject(prot, FIELD_PORT, Integer.class) + .ifPresent(data::setFunctionPort); + LoraUtils.getChildObject(prot, FIELD_SPREADING_FACTOR, Integer.class) + .ifPresent(data::setSpreadingFactor); + final GatewayInfo gwInfo = new GatewayInfo(); + LoraUtils.getChildObject(prot, FIELD_RSSI, Integer.class) + .ifPresent(gwInfo::setRssi); + LoraUtils.getChildObject(prot, FIELD_SNR, Double.class) + .ifPresent(gwInfo::setSnr); + return gwInfo; + }) + .ifPresent(data::addGatewayInfo); + + return data; + } } diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ProximusProvider.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ProximusProvider.java index f8b382a40e..955be4b8e7 100644 --- a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ProximusProvider.java +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ProximusProvider.java @@ -14,8 +14,11 @@ package org.eclipse.hono.adapter.lora.providers; import java.util.Objects; +import java.util.Optional; +import org.eclipse.hono.adapter.lora.GatewayInfo; import org.eclipse.hono.adapter.lora.LoraMessageType; +import org.eclipse.hono.adapter.lora.LoraMetaData; import org.springframework.stereotype.Component; import com.google.common.io.BaseEncoding; @@ -31,10 +34,18 @@ * Proximus data format. */ @Component -public class ProximusProvider extends BaseLoraProvider { +public class ProximusProvider extends JsonBasedLoraProvider { + private static final String FIELD_PROXIMUS_ADR = "Adrbit"; private static final String FIELD_PROXIMUS_DEVICE_EUI = "DevEUI"; + private static final String FIELD_PROXIMUS_FRAME_COUNT = "Fcntup"; + private static final String FIELD_PROXIMUS_LATITUDE = "latitude"; + private static final String FIELD_PROXIMUS_LONGITUDE = "longitude"; private static final String FIELD_PROXIMUS_PAYLOAD = "payload"; + private static final String FIELD_PROXIMUS_PORT = "FPort"; + private static final String FIELD_PROXIMUS_RSSI = "Lrrrssi"; + private static final String FIELD_PROXIMUS_SNR = "Lrrsnr"; + private static final String FIELD_PROXIMUS_SPREADING_FACTOR = "Spfact"; @Override public String getProviderName() { @@ -52,12 +63,12 @@ public String pathPrefix() { * @return Always {@link LoraMessageType#UPLINK}. */ @Override - protected LoraMessageType extractMessageType(final JsonObject loraMessage) { + protected LoraMessageType getMessageType(final JsonObject loraMessage) { return LoraMessageType.UPLINK; } @Override - protected String extractDevEui(final JsonObject loraMessage) { + protected String getDevEui(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); return LoraUtils.getChildObject(loraMessage, FIELD_PROXIMUS_DEVICE_EUI, String.class) @@ -65,7 +76,7 @@ protected String extractDevEui(final JsonObject loraMessage) { } @Override - protected Buffer extractPayload(final JsonObject loraMessage) { + protected Buffer getPayload(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); @@ -73,4 +84,41 @@ protected Buffer extractPayload(final JsonObject loraMessage) { .map(s -> Buffer.buffer(BaseEncoding.base16().decode(s.toUpperCase()))) .orElseThrow(() -> new LoraProviderMalformedPayloadException("message does not contain HEX encoded payload property")); } + + /** + * {@inheritDoc} + */ + @Override + protected LoraMetaData getMetaData(final JsonObject loraMessage) { + + Objects.requireNonNull(loraMessage); + final LoraMetaData data = new LoraMetaData(); + LoraUtils.getChildObject(loraMessage, FIELD_PROXIMUS_ADR, String.class) + .ifPresent(v -> data.setAdaptiveDataRateEnabled(v.equals("1") ? Boolean.TRUE : Boolean.FALSE)); + LoraUtils.getChildObject(loraMessage, FIELD_PROXIMUS_FRAME_COUNT, String.class) + .map(Integer::valueOf) + .ifPresent(data::setFrameCount); + LoraUtils.getChildObject(loraMessage, FIELD_PROXIMUS_PORT, String.class) + .map(Integer::valueOf) + .ifPresent(data::setFunctionPort); + LoraUtils.getChildObject(loraMessage, FIELD_PROXIMUS_SPREADING_FACTOR, String.class) + .map(Integer::valueOf) + .ifPresent(data::setSpreadingFactor); + Optional.ofNullable(LoraUtils.newLocationFromString( + LoraUtils.getChildObject(loraMessage, FIELD_PROXIMUS_LONGITUDE, String.class), + LoraUtils.getChildObject(loraMessage, FIELD_PROXIMUS_LATITUDE, String.class), + Optional.empty())) + .ifPresent(data::setLocation); + + final GatewayInfo gwInfo = new GatewayInfo(); + LoraUtils.getChildObject(loraMessage, FIELD_PROXIMUS_RSSI, String.class) + .map(Double::valueOf) + .map(Double::intValue) + .ifPresent(gwInfo::setRssi); + LoraUtils.getChildObject(loraMessage, FIELD_PROXIMUS_SNR, String.class) + .map(Double::valueOf) + .ifPresent(gwInfo::setSnr); + data.addGatewayInfo(gwInfo); + return data; + } } diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ThingsNetworkProvider.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ThingsNetworkProvider.java index 2d6924d61b..a900bf8b11 100644 --- a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ThingsNetworkProvider.java +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/ThingsNetworkProvider.java @@ -14,26 +14,48 @@ package org.eclipse.hono.adapter.lora.providers; import java.util.Base64; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; -import org.eclipse.hono.adapter.lora.LoraConstants; +import org.eclipse.hono.adapter.lora.GatewayInfo; import org.eclipse.hono.adapter.lora.LoraMessageType; +import org.eclipse.hono.adapter.lora.LoraMetaData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; /** * A LoRaWAN provider with API for Things Network. */ @Component -public class ThingsNetworkProvider extends BaseLoraProvider { +public class ThingsNetworkProvider extends JsonBasedLoraProvider { + private static final Logger LOG = LoggerFactory.getLogger(ThingsNetworkProvider.class); + private static final Pattern PATTERN_DATA_RATE = Pattern.compile("^SF(\\d+)BW(\\d+)$"); + + private static final String FIELD_TTN_ALTITUDE = "altitude"; + private static final String FIELD_TTN_CHANNEL = "channel"; + private static final String FIELD_TTN_CODING_RATE = "coding_rate"; + private static final String FIELD_TTN_DATA_RATE = "data_rate"; private static final String FIELD_TTN_DEVICE_EUI = "hardware_serial"; - private static final String FIELD_TTN_PAYLOAD_RAW = "payload_raw"; + private static final String FIELD_TTN_FRAME_COUNT = "counter"; + private static final String FIELD_TTN_FREQUENCY = "frequency"; private static final String FIELD_TTN_FPORT = "port"; + private static final String FIELD_TTN_GW_EUI = "gtw_id"; + private static final String FIELD_TTN_LATITUDE = "latitude"; + private static final String FIELD_TTN_LONGITUDE = "longitude"; + private static final String FIELD_TTN_PAYLOAD_RAW = "payload_raw"; + private static final String FIELD_TTN_RSSI = "rssi"; + private static final String FIELD_TTN_SNR = "snr"; + + private static final String OBJECT_GATEWAYS = "gateways"; + private static final String OBJECT_META_DATA = "metadata"; @Override public String getProviderName() { @@ -51,12 +73,12 @@ public String pathPrefix() { * @return Always {@link LoraMessageType#UPLINK}. */ @Override - protected LoraMessageType extractMessageType(final JsonObject loraMessage) { + protected LoraMessageType getMessageType(final JsonObject loraMessage) { return LoraMessageType.UPLINK; } @Override - protected String extractDevEui(final JsonObject loraMessage) { + protected String getDevEui(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); return LoraUtils.getChildObject(loraMessage, FIELD_TTN_DEVICE_EUI, String.class) @@ -64,7 +86,7 @@ protected String extractDevEui(final JsonObject loraMessage) { } @Override - protected Buffer extractPayload(final JsonObject loraMessage) { + protected Buffer getPayload(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); @@ -79,14 +101,63 @@ protected Buffer extractPayload(final JsonObject loraMessage) { } @Override - protected Map extractNormalizedData(final JsonObject loraMessage) { + protected LoraMetaData getMetaData(final JsonObject loraMessage) { Objects.requireNonNull(loraMessage); - final Map returnMap = new HashMap<>(); - - LoraUtils.addNormalizedValue(loraMessage, FIELD_TTN_FPORT, Integer.class, LoraConstants.APP_PROPERTY_FUNCTION_PORT, v -> v, returnMap); - - return returnMap; + final LoraMetaData data = new LoraMetaData(); + + LoraUtils.getChildObject(loraMessage, FIELD_TTN_FRAME_COUNT, Integer.class) + .ifPresent(data::setFrameCount); + LoraUtils.getChildObject(loraMessage, FIELD_TTN_FPORT, Integer.class) + .ifPresent(data::setFunctionPort); + + LoraUtils.getChildObject(loraMessage, OBJECT_META_DATA, JsonObject.class) + .map(meta -> { + + LoraUtils.getChildObject(meta, FIELD_TTN_CODING_RATE, String.class) + .ifPresent(data::setCodingRate); + + LoraUtils.getChildObject(meta, FIELD_TTN_DATA_RATE, String.class) + .ifPresent(dataRate -> { + final Matcher matcher = PATTERN_DATA_RATE.matcher(dataRate); + if (matcher.matches()) { + data.setSpreadingFactor(Integer.parseInt(matcher.group(1))); + data.setBandwidth(Integer.parseInt(matcher.group(2))); + } else { + LOG.debug("invalid data rate [{}]", dataRate); + } + }); + + LoraUtils.getChildObject(meta, FIELD_TTN_FREQUENCY, Double.class) + .ifPresent(data::setFrequency); + + return meta.getValue(OBJECT_GATEWAYS); + }) + .filter(JsonArray.class::isInstance) + .map(JsonArray.class::cast) + .ifPresent(gws -> { + gws.stream() + .filter(JsonObject.class::isInstance) + .map(JsonObject.class::cast) + .forEach(gw -> { + final GatewayInfo gwInfo = new GatewayInfo(); + LoraUtils.getChildObject(gw, FIELD_TTN_GW_EUI, String.class) + .ifPresent(gwInfo::setGatewayId); + LoraUtils.getChildObject(gw, FIELD_TTN_CHANNEL, Integer.class) + .ifPresent(gwInfo::setChannel); + LoraUtils.getChildObject(gw, FIELD_TTN_RSSI, Integer.class) + .ifPresent(gwInfo::setRssi); + LoraUtils.getChildObject(gw, FIELD_TTN_SNR, Double.class) + .ifPresent(gwInfo::setSnr); + Optional.ofNullable(LoraUtils.newLocation( + LoraUtils.getChildObject(gw, FIELD_TTN_LONGITUDE, Double.class), + LoraUtils.getChildObject(gw, FIELD_TTN_LATITUDE, Double.class), + LoraUtils.getChildObject(gw, FIELD_TTN_ALTITUDE, Double.class))) + .ifPresent(gwInfo::setLocation); + data.addGatewayInfo(gwInfo); + }); + }); + + return data; } - } diff --git a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/impl/LoraProtocolAdapterTest.java b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/impl/LoraProtocolAdapterTest.java index 6557d90686..a4595f7190 100644 --- a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/impl/LoraProtocolAdapterTest.java +++ b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/impl/LoraProtocolAdapterTest.java @@ -285,7 +285,7 @@ public void handleProviderRouteCausesUnauthorizedForInvalidGatewayCredentials() public void handleProviderRouteCausesBadRequestForFailureToParseBody() { final LoraProvider providerMock = getLoraProviderMock(); - when(providerMock.getMessage(any(Buffer.class))).thenThrow(new LoraProviderMalformedPayloadException("no device ID")); + when(providerMock.getMessage(any(RoutingContext.class))).thenThrow(new LoraProviderMalformedPayloadException("no device ID")); final RoutingContext routingContextMock = getRoutingContextMock(); adapter.handleProviderRoute(routingContextMock, providerMock); @@ -321,7 +321,7 @@ private LoraProvider getLoraProviderMock(final LoraMessage message) { final LoraProvider provider = mock(LoraProvider.class); when(provider.getProviderName()).thenReturn(TEST_PROVIDER); when(provider.pathPrefix()).thenReturn("/bumlux"); - when(provider.getMessage(any(Buffer.class))).thenReturn(message); + when(provider.getMessage(any(RoutingContext.class))).thenReturn(message); return provider; } diff --git a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/ActilityProviderTest.java b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/ActilityProviderTest.java index 4bfaa8f9f0..e5f193baa2 100644 --- a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/ActilityProviderTest.java +++ b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/ActilityProviderTest.java @@ -15,14 +15,8 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.util.Map; - -import org.eclipse.hono.adapter.lora.LoraConstants; +import org.eclipse.hono.adapter.lora.LoraMetaData; import org.eclipse.hono.adapter.lora.UplinkLoraMessage; -import org.junit.jupiter.api.Test; - -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; /** * Verifies behavior of {@link ActilityProvider}. @@ -39,21 +33,19 @@ protected ActilityProvider newProvider() { } /** - * Verifies that properties are parsed correctly from the lora message. + * {@inheritDoc} */ - @Test - public void testGetMessageParsesNormalizedProperties() { - - final UplinkLoraMessage loraMessage = (UplinkLoraMessage) provider.getMessage(uplinkMessageBuffer); - - final Map map = loraMessage.getNormalizedData(); - - assertThat(map.get(LoraConstants.APP_PROPERTY_RSS)).isEqualTo(48.0); + @Override + protected void assertMetaDataForUplinkMessage(final UplinkLoraMessage loraMessage) { - final JsonArray expectedArray = new JsonArray(); - expectedArray.add(new JsonObject().put("gateway_id", "18035559").put("rss", 48.0).put("snr", 3.0)); - expectedArray.add(new JsonObject().put("gateway_id", "18035560").put("rss", 49.0).put("snr", 4.0)); + final LoraMetaData data = loraMessage.getMetaData(); - assertThat(new JsonArray((String) map.get(LoraConstants.GATEWAYS))).isEqualTo(expectedArray); + assertThat(data.getGatewayInfo()).hasSize(2); + assertThat(data.getGatewayInfo().get(0).getGatewayId()).isEqualTo("18035559"); + assertThat(data.getGatewayInfo().get(0).getRssi()).isEqualTo(-48); + assertThat(data.getGatewayInfo().get(0).getSnr()).isEqualTo(3.0); + assertThat(data.getGatewayInfo().get(1).getGatewayId()).isEqualTo("18035560"); + assertThat(data.getGatewayInfo().get(1).getRssi()).isEqualTo(-49); + assertThat(data.getGatewayInfo().get(1).getSnr()).isEqualTo(4.0); } } diff --git a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/ChirpStackProviderTest.java b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/ChirpStackProviderTest.java index 20d76dbce2..dc4db5e8ea 100644 --- a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/ChirpStackProviderTest.java +++ b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/ChirpStackProviderTest.java @@ -14,17 +14,9 @@ package org.eclipse.hono.adapter.lora.providers; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.Map; - -import org.eclipse.hono.adapter.lora.LoraConstants; +import org.eclipse.hono.adapter.lora.LoraMetaData; import org.eclipse.hono.adapter.lora.UplinkLoraMessage; -import org.junit.jupiter.api.Test; - -import io.vertx.core.json.Json; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; /** * Verifies behavior of {@link ChirpStackProvider}. @@ -41,30 +33,24 @@ protected ChirpStackProvider newProvider() { } /** - * Verifies that properties are parsed correctly from the lora message. + * {@inheritDoc} */ - @Test - public void testGetMessageParsesProperties() { - - final UplinkLoraMessage loraMessage = (UplinkLoraMessage) provider.getMessage(uplinkMessageBuffer); - - final Map normalizedData = loraMessage.getNormalizedData(); - assertThat(normalizedData).contains(Map.entry(LoraConstants.APP_PROPERTY_FUNCTION_PORT, 5)); - assertThat(normalizedData).contains(Map.entry(LoraConstants.FRAME_COUNT, 10)); - assertThat(normalizedData).contains(Map.entry(LoraConstants.APP_PROPERTY_SPREADING_FACTOR, 11)); - assertThat(normalizedData).contains(Map.entry(LoraConstants.APP_PROPERTY_BANDWIDTH, 125)); - assertThat(normalizedData).contains(Map.entry(LoraConstants.CODING_RATE, "4/5")); - assertThat(normalizedData).contains(Map.entry(LoraConstants.FREQUENCY, 868100000)); - - final String gatewaysString = (String) normalizedData.get(LoraConstants.GATEWAYS); - final JsonArray gateways = (JsonArray) Json.decodeValue(gatewaysString); - final JsonObject gateway = gateways.getJsonObject(0); - - assertEquals(4.9144401, gateway.getDouble(LoraConstants.APP_PROPERTY_FUNCTION_LONGITUDE)); - assertEquals(52.3740364, gateway.getDouble(LoraConstants.APP_PROPERTY_FUNCTION_LATITUDE)); - assertEquals("0303030303030303", gateway.getString(LoraConstants.GATEWAY_ID)); - assertEquals(9, gateway.getDouble(LoraConstants.APP_PROPERTY_SNR)); - assertEquals(5, gateway.getInteger(LoraConstants.APP_PROPERTY_CHANNEL)); - assertEquals(-48, gateway.getInteger(LoraConstants.APP_PROPERTY_RSS)); + @Override + protected void assertMetaDataForUplinkMessage(final UplinkLoraMessage loraMessage) { + + final LoraMetaData metaData = loraMessage.getMetaData(); + assertThat(metaData.getFunctionPort()).isEqualTo(5); + assertThat(metaData.getFrameCount()).isEqualTo(10); + assertThat(metaData.getSpreadingFactor()).isEqualTo(11); + assertThat(metaData.getBandwidth()).isEqualTo(125); + assertThat(metaData.getFrequency()).isEqualTo(868.1); + + assertThat(metaData.getGatewayInfo()).hasSize(1); + assertThat(metaData.getGatewayInfo().get(0).getGatewayId()).isEqualTo("0303030303030303"); + assertThat(metaData.getGatewayInfo().get(0).getSnr()).isEqualTo(9.0); + assertThat(metaData.getGatewayInfo().get(0).getRssi()).isEqualTo(-48); + assertThat(metaData.getGatewayInfo().get(0).getLocation().getLongitude()).isEqualTo(4.9144401); + assertThat(metaData.getGatewayInfo().get(0).getLocation().getLatitude()).isEqualTo(52.3740364); + assertThat(metaData.getGatewayInfo().get(0).getLocation().getAltitude()).isEqualTo(10.5); } } diff --git a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/EverynetProviderTest.java b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/EverynetProviderTest.java index cbce57c612..78f87c4017 100644 --- a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/EverynetProviderTest.java +++ b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/EverynetProviderTest.java @@ -13,6 +13,11 @@ package org.eclipse.hono.adapter.lora.providers; +import static org.assertj.core.api.Assertions.assertThat; + +import org.eclipse.hono.adapter.lora.LoraMetaData; +import org.eclipse.hono.adapter.lora.UplinkLoraMessage; + /** * Verifies behavior of {@link EverynetProvider}. */ @@ -25,4 +30,26 @@ public class EverynetProviderTest extends LoraProviderTestBase protected EverynetProvider newProvider() { return new EverynetProvider(); } + + /** + * {@inheritDoc} + */ + @Override + protected void assertMetaDataForUplinkMessage(final UplinkLoraMessage loraMessage) { + + final LoraMetaData metaData = loraMessage.getMetaData(); + assertThat(metaData.getFunctionPort()).isEqualTo(7); + assertThat(metaData.getFrameCount()).isEqualTo(8); + assertThat(metaData.getFrequency()).isEqualTo(868.1); + assertThat(metaData.getSpreadingFactor()).isEqualTo(12); + assertThat(metaData.getBandwidth()).isEqualTo(125); + + assertThat(metaData.getGatewayInfo()).hasSize(1); + assertThat(metaData.getGatewayInfo().get(0).getChannel()).isEqualTo(0); + assertThat(metaData.getGatewayInfo().get(0).getRssi()).isEqualTo(-100); + assertThat(metaData.getGatewayInfo().get(0).getSnr()).isEqualTo(5.0); + assertThat(metaData.getGatewayInfo().get(0).getLocation().getLongitude()).isEqualTo(30.258167266845703); + assertThat(metaData.getGatewayInfo().get(0).getLocation().getLatitude()).isEqualTo(59.890445709228516); + assertThat(metaData.getGatewayInfo().get(0).getLocation().getAltitude()).isNull(); + } } diff --git a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/FireflyProviderTest.java b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/FireflyProviderTest.java index b9e3cab13c..d36fa226c1 100644 --- a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/FireflyProviderTest.java +++ b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/FireflyProviderTest.java @@ -15,11 +15,8 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.util.Map; - -import org.eclipse.hono.adapter.lora.LoraConstants; +import org.eclipse.hono.adapter.lora.LoraMetaData; import org.eclipse.hono.adapter.lora.UplinkLoraMessage; -import org.junit.jupiter.api.Test; /** * Verifies behavior of {@link FireflyProvider}. @@ -35,14 +32,25 @@ protected FireflyProvider newProvider() { } /** - * Verifies that properties are parsed correctly from the lora message. + * {@inheritDoc} */ - @Test - public void testGetMessageParsesNormalizedData() { - - final UplinkLoraMessage loraMessage = (UplinkLoraMessage) provider.getMessage(uplinkMessageBuffer); - - final Map map = loraMessage.getNormalizedData(); - assertThat(map.get(LoraConstants.APP_PROPERTY_MIC)).isEqualTo(Boolean.TRUE); + @Override + protected void assertMetaDataForUplinkMessage(final UplinkLoraMessage loraMessage) { + + final LoraMetaData metaData = loraMessage.getMetaData(); + assertThat(metaData.getAdaptiveDataRateEnabled()); + assertThat(metaData.getBandwidth()).isEqualTo(125); + assertThat(metaData.getCodingRate()).isEqualTo("4/5"); + assertThat(metaData.getFrameCount()).isEqualTo(2602); + assertThat(metaData.getFrequency()).isEqualTo(868.3); + assertThat(metaData.getFunctionPort()).isEqualTo(2); + assertThat(metaData.getSpreadingFactor()).isEqualTo(7); + assertThat(metaData.getGatewayInfo()).hasSize(2); + assertThat(metaData.getGatewayInfo().get(0).getGatewayId()).isEqualTo("0101010101010101"); + assertThat(metaData.getGatewayInfo().get(0).getRssi()).isEqualTo(-84); + assertThat(metaData.getGatewayInfo().get(0).getSnr()).isEqualTo(9.8); + assertThat(metaData.getGatewayInfo().get(1).getGatewayId()).isEqualTo("0202020202020202"); + assertThat(metaData.getGatewayInfo().get(1).getRssi()).isEqualTo(-116); + assertThat(metaData.getGatewayInfo().get(1).getSnr()).isEqualTo(-3.2); } } diff --git a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/LoraProviderTestBase.java b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/LoraProviderTestBase.java index 3a24ccc5e0..1d1ca856a3 100644 --- a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/LoraProviderTestBase.java +++ b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/LoraProviderTestBase.java @@ -14,7 +14,10 @@ package org.eclipse.hono.adapter.lora.providers; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.io.IOException; import java.nio.charset.StandardCharsets; import org.eclipse.hono.adapter.lora.LoraMessageType; @@ -23,6 +26,8 @@ import org.junit.jupiter.api.Test; import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.ext.web.RoutingContext; /** * Base class for implementing tests for {@link LoraProvider} implementations. @@ -36,11 +41,6 @@ public abstract class LoraProviderTestBase { */ protected T provider; - /** - * The buffer containing the uplink message to use for testing. - */ - protected Buffer uplinkMessageBuffer; - /** * Creates a new instance of the provider under test. * @@ -48,6 +48,24 @@ public abstract class LoraProviderTestBase { */ protected abstract T newProvider(); + /** + * Creates a routing context representing a request from a provider's network server. + * + * @param type The type of message to include in the request. + * @param classifiers The classifiers to use for loading the request message from the file system. + * @return The routing context. + * @throws IOException If the file containing the example message could not be loaded. + */ + protected final RoutingContext getRequestContext(final LoraMessageType type, final String... classifiers) throws IOException { + + final Buffer message = LoraTestUtil.loadTestFile(provider.getProviderName(), LoraMessageType.UPLINK, classifiers); + final HttpServerRequest request = mock(HttpServerRequest.class); + final RoutingContext routingContext = mock(RoutingContext.class); + when(routingContext.request()).thenReturn(request); + when(routingContext.getBody()).thenReturn(message); + return routingContext; + } + /** * Sets up the fixture. * @@ -56,18 +74,45 @@ public abstract class LoraProviderTestBase { @BeforeEach public void setUp() throws Exception { provider = newProvider(); - uplinkMessageBuffer = LoraTestUtil.loadTestFile(provider.getProviderName(), LoraMessageType.UPLINK); } /** - * Verifies that common properties are parsed correctly from the lora message. + * Verifies that uplink messages are parsed correctly. + * + * @throws IOException If the file containing the example message could not be loaded. */ @Test - public void testGetMessageParsesCommonUplinkMessageProperties() { + public void testGetMessageSucceedsForUplinkMessage() throws IOException { - final UplinkLoraMessage loraMessage = (UplinkLoraMessage) provider.getMessage(uplinkMessageBuffer); + final RoutingContext request = getRequestContext(LoraMessageType.UPLINK); + final UplinkLoraMessage loraMessage = (UplinkLoraMessage) provider.getMessage(request); + assertCommonUplinkProperties(loraMessage); + assertMetaDataForUplinkMessage(loraMessage); + } - assertThat(loraMessage.getDevEUIAsString()).isEqualTo("0102030405060708"); - assertThat(loraMessage.getPayload().getBytes()).isEqualTo("bumlux".getBytes(StandardCharsets.UTF_8)); + /** + * Asserts presence of common properties in an uplink message. + * + * @param uplinkMessage The message to assert. + */ + protected void assertCommonUplinkProperties(final UplinkLoraMessage uplinkMessage) { + assertThat(uplinkMessage.getDevEUIAsString()).isEqualTo("0102030405060708"); + assertThat(uplinkMessage.getPayload().getBytes()).isEqualTo("bumlux".getBytes(StandardCharsets.UTF_8)); + } + + /** + * Asserts presence of meta data in an uplink message. + *

+ * This method is invoked as part of the {@link #testGetMessageSucceedsForUplinkMessage()} test. + * Subclasses should override this method in order to verify properties that are supported + * by the particular provider. + *

+ * This default implementation does nothing. + * + * @param uplinkMessage The message to assert. + * @throws AssertionError if any property fails assertion. + */ + protected void assertMetaDataForUplinkMessage(final UplinkLoraMessage uplinkMessage) { + // do nothing } } diff --git a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/LoraTestUtil.java b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/LoraTestUtil.java index 8262d1d5d1..418f8ccef4 100644 --- a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/LoraTestUtil.java +++ b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/LoraTestUtil.java @@ -44,9 +44,8 @@ private LoraTestUtil() { * @param classifiers additional classifiers of the test file. * @return the contents of the file. * @throws IOException if the test file could not be loaded. - * @throws URISyntaxException if the test file could not be loaded. */ - public static Buffer loadTestFile(final String providerName, final LoraMessageType type, final String... classifiers) throws IOException, URISyntaxException { + public static Buffer loadTestFile(final String providerName, final LoraMessageType type, final String... classifiers) throws IOException { Objects.requireNonNull(providerName); Objects.requireNonNull(type); final String name = Stream @@ -54,8 +53,13 @@ public static Buffer loadTestFile(final String providerName, final LoraMessageTy Stream.of(providerName, type.name().toLowerCase()), Arrays.stream(classifiers)) .collect(Collectors.joining(".")); - final URL location = LoraTestUtil.class.getResource(String.format("/payload/%s.json", name)); - return Buffer.buffer(Files.readAllBytes(Paths.get(location.toURI()))); + try { + final URL location = LoraTestUtil.class.getResource(String.format("/payload/%s.json", name)); + return Buffer.buffer(Files.readAllBytes(Paths.get(location.toURI()))); + } catch (final URISyntaxException e) { + // cannot happen because the URL is created by the class loader + return null; + } } } diff --git a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/LoriotProviderTest.java b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/LoriotProviderTest.java index abbeb62718..c67e57937d 100644 --- a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/LoriotProviderTest.java +++ b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/LoriotProviderTest.java @@ -13,6 +13,11 @@ package org.eclipse.hono.adapter.lora.providers; +import static org.assertj.core.api.Assertions.assertThat; + +import org.eclipse.hono.adapter.lora.LoraMetaData; +import org.eclipse.hono.adapter.lora.UplinkLoraMessage; + /** * Verifies behavior of {@link LoriotProvider}. */ @@ -25,4 +30,25 @@ public class LoriotProviderTest extends LoraProviderTestBase { protected LoriotProvider newProvider() { return new LoriotProvider(); } + + /** + * {@inheritDoc} + */ + @Override + protected void assertMetaDataForUplinkMessage(final UplinkLoraMessage loraMessage) { + + final LoraMetaData metaData = loraMessage.getMetaData(); + assertThat(metaData.getBandwidth()).isEqualTo(125); + assertThat(metaData.getCodingRate()).isEqualTo("4/5"); + assertThat(metaData.getFrameCount()).isEqualTo(135); + assertThat(metaData.getFrequency()).isEqualTo(868.3); + assertThat(metaData.getFunctionPort()).isEqualTo(2); + assertThat(metaData.getSpreadingFactor()).isEqualTo(7); + assertThat(metaData.getGatewayInfo()).hasSize(1); + assertThat(metaData.getGatewayInfo().get(0).getGatewayId()).isEqualTo("0101010101010101"); + assertThat(metaData.getGatewayInfo().get(0).getRssi()).isEqualTo(-63); + assertThat(metaData.getGatewayInfo().get(0).getSnr()).isEqualTo(10); + assertThat(metaData.getGatewayInfo().get(0).getLocation().getLongitude()).isEqualTo(4.4007817); + assertThat(metaData.getGatewayInfo().get(0).getLocation().getLatitude()).isEqualTo(12); + } } diff --git a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/ObjeniousProviderTest.java b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/ObjeniousProviderTest.java index bedbdc3572..1b6df7cdd6 100644 --- a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/ObjeniousProviderTest.java +++ b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/ObjeniousProviderTest.java @@ -13,6 +13,11 @@ package org.eclipse.hono.adapter.lora.providers; +import static org.assertj.core.api.Assertions.assertThat; + +import org.eclipse.hono.adapter.lora.LoraMetaData; +import org.eclipse.hono.adapter.lora.UplinkLoraMessage; + /** * Verifies behavior of {@link ObjeniousProvider}. */ @@ -26,4 +31,21 @@ public class ObjeniousProviderTest extends LoraProviderTestBase protected ProximusProvider newProvider() { return new ProximusProvider(); } + + /** + * {@inheritDoc} + */ + @Override + protected void assertMetaDataForUplinkMessage(final UplinkLoraMessage loraMessage) { + + final LoraMetaData metaData = loraMessage.getMetaData(); + assertThat(metaData.getFrameCount()).isEqualTo(23); + assertThat(metaData.getFunctionPort()).isEqualTo(6); + assertThat(metaData.getLocation()).isNull(); + assertThat(metaData.getSpreadingFactor()).isEqualTo(11); + assertThat(metaData.getGatewayInfo()).hasSize(1); + assertThat(metaData.getGatewayInfo().get(0).getSnr()).isEqualTo(2.0); + assertThat(metaData.getGatewayInfo().get(0).getRssi()).isEqualTo(-54); + } } diff --git a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/ThingsNetworkProviderTest.java b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/ThingsNetworkProviderTest.java index ef205ac487..ae63dc4a2a 100644 --- a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/ThingsNetworkProviderTest.java +++ b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/ThingsNetworkProviderTest.java @@ -15,10 +15,15 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.io.IOException; + import org.eclipse.hono.adapter.lora.LoraMessageType; +import org.eclipse.hono.adapter.lora.LoraMetaData; import org.eclipse.hono.adapter.lora.UplinkLoraMessage; import org.junit.jupiter.api.Test; +import io.vertx.ext.web.RoutingContext; + /** * Verifies behavior of {@link ThingsNetworkProvider}. */ @@ -36,16 +41,40 @@ protected ThingsNetworkProvider newProvider() { /** * Test an uplink message with a null payload. * - * @throws Exception if anything goes wrong. + * @throws IOException If the file containing the example message could not be loaded. */ @Test - public void testGetMessageParsesUplinkMessagePropertiesWithNullPayload() throws Exception { + public void testGetMessageParsesUplinkMessagePropertiesWithNullPayload() throws IOException { - final UplinkLoraMessage loraMessage = (UplinkLoraMessage) this.provider.getMessage(LoraTestUtil.loadTestFile(this.provider.getProviderName(), LoraMessageType.UPLINK, "with-null-payload")); + final RoutingContext requestContext = getRequestContext(LoraMessageType.UPLINK, "with-null-payload"); + final UplinkLoraMessage loraMessage = (UplinkLoraMessage) provider.getMessage(requestContext); assertThat(loraMessage.getDevEUIAsString()).isEqualTo("0102030405060708"); assertThat(loraMessage.getPayload()).isNotNull(); assertThat(loraMessage.getPayload().length()).isEqualTo(0); } + /** + * {@inheritDoc} + */ + @Override + protected void assertMetaDataForUplinkMessage(final UplinkLoraMessage loraMessage) { + + final LoraMetaData metaData = loraMessage.getMetaData(); + assertThat(metaData.getFunctionPort()).isEqualTo(1); + assertThat(metaData.getFrameCount()).isEqualTo(9); + assertThat(metaData.getSpreadingFactor()).isEqualTo(7); + assertThat(metaData.getBandwidth()).isEqualTo(125); + assertThat(metaData.getFrequency()).isEqualTo(868.1); + assertThat(metaData.getCodingRate()).isEqualTo("4/5"); + + assertThat(metaData.getGatewayInfo()).hasSize(1); + assertThat(metaData.getGatewayInfo().get(0).getGatewayId()).isEqualTo("0203040506070809"); + assertThat(metaData.getGatewayInfo().get(0).getChannel()).isEqualTo(0); + assertThat(metaData.getGatewayInfo().get(0).getSnr()).isEqualTo(5.0); + assertThat(metaData.getGatewayInfo().get(0).getRssi()).isEqualTo(-25); + assertThat(metaData.getGatewayInfo().get(0).getLocation().getLongitude()).isEqualTo(9.1934); + assertThat(metaData.getGatewayInfo().get(0).getLocation().getLatitude()).isEqualTo(53.1088); + assertThat(metaData.getGatewayInfo().get(0).getLocation().getAltitude()).isEqualTo(90.0); + } } diff --git a/adapters/lora-vertx/src/test/resources/payload/everynet.uplink.json b/adapters/lora-vertx/src/test/resources/payload/everynet.uplink.json index f851d1c186..2f141769e8 100644 --- a/adapters/lora-vertx/src/test/resources/payload/everynet.uplink.json +++ b/adapters/lora-vertx/src/test/resources/payload/everynet.uplink.json @@ -5,7 +5,30 @@ "duplicate": false, "counter_up": 8, "rx_time": 1559072813.337586, - "encrypted_payload": "YnVtbHV4=" + "encrypted_payload": "YnVtbHV4=", + "radio": { + "modulation": { + "bandwidth": 125000, + "type": 0, + "spreading": 12, + "coderate": "4/7" + }, + "hardware": { + "status": 1, + "chain": 0, + "tmst": 514586002, + "snr": 5.0, + "rssi": -100.0, + "channel": 0, + "gps": { + "lat": 59.890445709228516, + "lng": 30.258167266845703 + } + }, + "freq": 868.1, + "datarate": 0, + "time": 1504806731.249041 + } }, "meta": { "network": "7a068679f9d8528b0ac02fe7ca8982c6", diff --git a/adapters/lora-vertx/src/test/resources/payload/firefly.uplink.json b/adapters/lora-vertx/src/test/resources/payload/firefly.uplink.json index 2f2c937031..239c15ab21 100644 --- a/adapters/lora-vertx/src/test/resources/payload/firefly.uplink.json +++ b/adapters/lora-vertx/src/test/resources/payload/firefly.uplink.json @@ -27,7 +27,7 @@ "srv_rcv_time":1567086092891409, "rssi":-84, "lsnr":9.8, - "gweui":"gw-1" + "gweui":"0101010101010101" }, { "tmst":1909379987, @@ -35,7 +35,7 @@ "srv_rcv_time":1567086092899587, "rssi":-116, "lsnr":-3.2, - "gweui":"gw-2" + "gweui":"0202020202020202" } ], "freq":868.3, diff --git a/adapters/lora-vertx/src/test/resources/payload/loriot.uplink.json b/adapters/lora-vertx/src/test/resources/payload/loriot.uplink.json index 1f1dda40b4..1cdec9b4c0 100644 --- a/adapters/lora-vertx/src/test/resources/payload/loriot.uplink.json +++ b/adapters/lora-vertx/src/test/resources/payload/loriot.uplink.json @@ -14,7 +14,7 @@ "snr": 10, "ts": 1573574479110, "time": "2019-11-12T16:01:19.103417Z", - "gweui": "gateway", + "gweui": "0101010101010101", "lat": 12, "lon": 4.4007817 } diff --git a/adapters/lora-vertx/src/test/resources/payload/ttn.uplink.json b/adapters/lora-vertx/src/test/resources/payload/ttn.uplink.json index da14c067ff..2b1ee838e7 100644 --- a/adapters/lora-vertx/src/test/resources/payload/ttn.uplink.json +++ b/adapters/lora-vertx/src/test/resources/payload/ttn.uplink.json @@ -18,7 +18,7 @@ "coding_rate": "4/5", "gateways": [ { - "gtw_id": "ttn-test-gtw", + "gtw_id": "0203040506070809", "timestamp": 12345, "time": "2019-03-03T04:19:32Z", "channel": 0,