first-fix
This commit is contained in:
1 parent
6d87c1edc8
commit
565d9d673a
6284 files changed
+820267
-76
No files matched your search
@@ -0,0 +1,108 @@
|
||||
<!--
|
||||
|
||||
Copyright © 2016-2023 The Thingsboard Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.thingsboard.common</groupId>
|
||||
<version>0.6.2-SNAPSHOT</version>
|
||||
<artifactId>transport</artifactId>
|
||||
</parent>
|
||||
<groupId>org.thingsboard.common.transport</groupId>
|
||||
<artifactId>coap</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Thingsboard CoAP Transport Common</name>
|
||||
<url>https://thingsboard.io</url>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<main.dir>${basedir}/../../..</main.dir>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.thingsboard.common.transport</groupId>
|
||||
<artifactId>transport-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.thingsboard.common</groupId>
|
||||
<artifactId>coap-server</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.californium</groupId>
|
||||
<artifactId>californium-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.californium</groupId>
|
||||
<artifactId>scandium</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context-support</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>log4j-over-slf4j</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.vintage</groupId>
|
||||
<artifactId>junit-vintage-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.awaitility</groupId>
|
||||
<artifactId>awaitility</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.xolstice.maven.plugins</groupId>
|
||||
<artifactId>protobuf-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.CoapResource;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
import org.thingsboard.server.common.transport.TransportService;
|
||||
import org.thingsboard.server.common.transport.TransportServiceCallback;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||
|
||||
@Slf4j
|
||||
public abstract class AbstractCoapTransportResource extends CoapResource {
|
||||
|
||||
protected final CoapTransportContext transportContext;
|
||||
protected final TransportService transportService;
|
||||
|
||||
public AbstractCoapTransportResource(CoapTransportContext context, String name) {
|
||||
super(name);
|
||||
this.transportContext = context;
|
||||
this.transportService = context.getTransportService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleGET(CoapExchange exchange) {
|
||||
processHandleGet(exchange);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePOST(CoapExchange exchange) {
|
||||
processHandlePost(exchange);
|
||||
}
|
||||
|
||||
protected abstract void processHandleGet(CoapExchange exchange);
|
||||
|
||||
protected abstract void processHandlePost(CoapExchange exchange);
|
||||
|
||||
protected void reportSubscriptionInfo(TransportProtos.SessionInfoProto sessionInfo, boolean hasAttributeSubscription, boolean hasRpcSubscription) {
|
||||
transportContext.getTransportService().process(sessionInfo, TransportProtos.SubscriptionInfoProto.newBuilder()
|
||||
.setAttributeSubscription(hasAttributeSubscription)
|
||||
.setRpcSubscription(hasRpcSubscription)
|
||||
.setLastActivityTime(System.currentTimeMillis())
|
||||
.build(), TransportServiceCallback.EMPTY);
|
||||
}
|
||||
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.thingsboard.server.common.transport.TransportContext;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||
import org.thingsboard.server.transport.coap.adaptors.JsonCoapAdaptor;
|
||||
import org.thingsboard.server.transport.coap.adaptors.ProtoCoapAdaptor;
|
||||
import org.thingsboard.server.transport.coap.client.CoapClientContext;
|
||||
import org.thingsboard.server.transport.coap.efento.adaptor.EfentoCoapAdaptor;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
|
||||
/**
|
||||
* Created by ashvayka on 18.10.18.
|
||||
*/
|
||||
@Slf4j
|
||||
@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.coap.enabled}'=='true')")
|
||||
@Component
|
||||
@Getter
|
||||
public class CoapTransportContext extends TransportContext {
|
||||
|
||||
@Value("${transport.sessions.report_timeout}")
|
||||
private long sessionReportTimeout;
|
||||
|
||||
@Autowired
|
||||
private JsonCoapAdaptor jsonCoapAdaptor;
|
||||
|
||||
@Autowired
|
||||
private ProtoCoapAdaptor protoCoapAdaptor;
|
||||
|
||||
@Autowired
|
||||
private EfentoCoapAdaptor efentoCoapAdaptor;
|
||||
|
||||
@Autowired
|
||||
private CoapClientContext clientContext;
|
||||
|
||||
private final ConcurrentMap<Integer, TransportProtos.ToDeviceRpcRequestMsg> rpcAwaitingAck = new ConcurrentHashMap<>();
|
||||
|
||||
}
|
||||
+484
@@ -0,0 +1,484 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.coap.CoAP;
|
||||
import org.eclipse.californium.core.coap.Request;
|
||||
import org.eclipse.californium.core.coap.Response;
|
||||
import org.eclipse.californium.core.network.Exchange;
|
||||
import org.eclipse.californium.core.observe.ObserveRelation;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
import org.eclipse.californium.core.server.resources.Resource;
|
||||
import org.eclipse.californium.core.server.resources.ResourceObserver;
|
||||
import org.thingsboard.server.coapserver.CoapServerService;
|
||||
import org.thingsboard.server.coapserver.TbCoapDtlsSessionInfo;
|
||||
import org.thingsboard.server.common.data.DataConstants;
|
||||
import org.thingsboard.server.common.data.DeviceProfile;
|
||||
import org.thingsboard.server.common.data.DeviceTransportType;
|
||||
import org.thingsboard.server.common.data.TransportPayloadType;
|
||||
import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
|
||||
import org.thingsboard.server.common.msg.session.FeatureType;
|
||||
import org.thingsboard.server.common.msg.session.SessionMsgType;
|
||||
import org.thingsboard.server.common.transport.TransportServiceCallback;
|
||||
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
|
||||
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
|
||||
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||
import org.thingsboard.server.transport.coap.callback.CoapDeviceAuthCallback;
|
||||
import org.thingsboard.server.transport.coap.callback.CoapNoOpCallback;
|
||||
import org.thingsboard.server.transport.coap.callback.CoapOkCallback;
|
||||
import org.thingsboard.server.transport.coap.callback.GetAttributesSyncSessionCallback;
|
||||
import org.thingsboard.server.transport.coap.callback.ToServerRpcSyncSessionCallback;
|
||||
import org.thingsboard.server.transport.coap.client.CoapClientContext;
|
||||
import org.thingsboard.server.transport.coap.client.TbCoapClientState;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.eclipse.californium.elements.DtlsEndpointContext.KEY_SESSION_ID;
|
||||
|
||||
@Slf4j
|
||||
public class CoapTransportResource extends AbstractCoapTransportResource {
|
||||
private static final int ACCESS_TOKEN_POSITION = 3;
|
||||
private static final int FEATURE_TYPE_POSITION = 4;
|
||||
private static final int REQUEST_ID_POSITION = 5;
|
||||
|
||||
private static final int FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST = 3;
|
||||
private static final int REQUEST_ID_POSITION_CERTIFICATE_REQUEST = 4;
|
||||
|
||||
private final ConcurrentMap<InetSocketAddress, TbCoapDtlsSessionInfo> dtlsSessionsMap;
|
||||
private final long timeout;
|
||||
private final long piggybackTimeout;
|
||||
private final CoapClientContext clients;
|
||||
|
||||
public CoapTransportResource(CoapTransportContext ctx, CoapServerService coapServerService, String name) {
|
||||
super(ctx, name);
|
||||
this.setObservable(true); // enable observing
|
||||
this.addObserver(new CoapResourceObserver());
|
||||
this.dtlsSessionsMap = coapServerService.getDtlsSessionsMap();
|
||||
this.timeout = coapServerService.getTimeout();
|
||||
this.piggybackTimeout = coapServerService.getPiggybackTimeout();
|
||||
this.clients = ctx.getClientContext();
|
||||
long sessionReportTimeout = ctx.getSessionReportTimeout();
|
||||
ctx.getScheduler().scheduleAtFixedRate(clients::reportActivity, new Random().nextInt((int) sessionReportTimeout), sessionReportTimeout, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/*
|
||||
* Overwritten method from CoapResource to be able to manage our own observe notification counters.
|
||||
*/
|
||||
@Override
|
||||
public void checkObserveRelation(Exchange exchange, Response response) {
|
||||
String token = getTokenFromRequest(exchange.getRequest());
|
||||
final ObserveRelation relation = exchange.getRelation();
|
||||
if (relation == null || relation.isCanceled()) {
|
||||
return; // because request did not try to establish a relation
|
||||
}
|
||||
if (response.getCode().isSuccess()) {
|
||||
if (!relation.isEstablished()) {
|
||||
relation.setEstablished();
|
||||
addObserveRelation(relation);
|
||||
}
|
||||
AtomicInteger state = clients.getNotificationCounterByToken(token);
|
||||
if (state != null) {
|
||||
response.getOptions().setObserve(state.getAndIncrement());
|
||||
} else {
|
||||
response.getOptions().removeObserve();
|
||||
}
|
||||
} // ObserveLayer takes care of the else case
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processHandleGet(CoapExchange exchange) {
|
||||
Optional<FeatureType> featureType = getFeatureType(exchange.advanced().getRequest());
|
||||
if (featureType.isEmpty()) {
|
||||
log.trace("Missing feature type parameter");
|
||||
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
|
||||
} else if (featureType.get() == FeatureType.TELEMETRY) {
|
||||
log.trace("Can't fetch/subscribe to timeseries updates");
|
||||
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
|
||||
} else if (exchange.getRequestOptions().hasObserve()) {
|
||||
processExchangeGetRequest(exchange, featureType.get());
|
||||
} else if (featureType.get() == FeatureType.ATTRIBUTES) {
|
||||
processRequest(exchange, SessionMsgType.GET_ATTRIBUTES_REQUEST);
|
||||
} else {
|
||||
log.trace("Invalid feature type parameter");
|
||||
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private void processExchangeGetRequest(CoapExchange exchange, FeatureType featureType) {
|
||||
boolean unsubscribe = exchange.getRequestOptions().getObserve() == 1;
|
||||
SessionMsgType sessionMsgType;
|
||||
if (featureType == FeatureType.RPC) {
|
||||
sessionMsgType = unsubscribe ? SessionMsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST : SessionMsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST;
|
||||
} else {
|
||||
sessionMsgType = unsubscribe ? SessionMsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST : SessionMsgType.SUBSCRIBE_ATTRIBUTES_REQUEST;
|
||||
}
|
||||
processRequest(exchange, sessionMsgType);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processHandlePost(CoapExchange exchange) {
|
||||
Optional<FeatureType> featureType = getFeatureType(exchange.advanced().getRequest());
|
||||
if (featureType.isEmpty()) {
|
||||
log.trace("Missing feature type parameter");
|
||||
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
|
||||
} else {
|
||||
switch (featureType.get()) {
|
||||
case ATTRIBUTES:
|
||||
processRequest(exchange, SessionMsgType.POST_ATTRIBUTES_REQUEST);
|
||||
break;
|
||||
case TELEMETRY:
|
||||
processRequest(exchange, SessionMsgType.POST_TELEMETRY_REQUEST);
|
||||
break;
|
||||
case RPC:
|
||||
Optional<Integer> requestId = getRequestId(exchange.advanced().getRequest());
|
||||
if (requestId.isPresent()) {
|
||||
processRequest(exchange, SessionMsgType.TO_DEVICE_RPC_RESPONSE);
|
||||
} else {
|
||||
processRequest(exchange, SessionMsgType.TO_SERVER_RPC_REQUEST);
|
||||
}
|
||||
break;
|
||||
case CLAIM:
|
||||
processRequest(exchange, SessionMsgType.CLAIM_REQUEST);
|
||||
break;
|
||||
case PROVISION:
|
||||
processProvision(exchange);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processProvision(CoapExchange exchange) {
|
||||
deferAccept(exchange);
|
||||
try {
|
||||
UUID sessionId = UUID.randomUUID();
|
||||
log.trace("[{}] Processing provision publish msg [{}]!", sessionId, exchange.advanced().getRequest());
|
||||
TransportProtos.ProvisionDeviceRequestMsg provisionRequestMsg;
|
||||
TransportPayloadType payloadType;
|
||||
try {
|
||||
provisionRequestMsg = transportContext.getJsonCoapAdaptor().convertToProvisionRequestMsg(sessionId, exchange.advanced().getRequest());
|
||||
payloadType = TransportPayloadType.JSON;
|
||||
} catch (Exception e) {
|
||||
if (e instanceof JsonParseException || (e.getCause() != null && e.getCause() instanceof JsonParseException)) {
|
||||
provisionRequestMsg = transportContext.getProtoCoapAdaptor().convertToProvisionRequestMsg(sessionId, exchange.advanced().getRequest());
|
||||
payloadType = TransportPayloadType.PROTOBUF;
|
||||
} else {
|
||||
throw new AdaptorException(e);
|
||||
}
|
||||
}
|
||||
transportService.process(provisionRequestMsg, new DeviceProvisionCallback(exchange, payloadType));
|
||||
} catch (AdaptorException e) {
|
||||
log.trace("Failed to decode message: ", e);
|
||||
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private void processRequest(CoapExchange exchange, SessionMsgType type) {
|
||||
log.trace("Processing {}", exchange.advanced().getRequest());
|
||||
deferAccept(exchange);
|
||||
Exchange advanced = exchange.advanced();
|
||||
Request request = advanced.getRequest();
|
||||
|
||||
var dtlsSessionId = request.getSourceContext().get(KEY_SESSION_ID);
|
||||
if (dtlsSessionsMap != null && dtlsSessionId != null && !dtlsSessionId.isEmpty()) {
|
||||
TbCoapDtlsSessionInfo tbCoapDtlsSessionInfo = dtlsSessionsMap
|
||||
.computeIfPresent(request.getSourceContext().getPeerAddress(), (dtlsSessionIdStr, dtlsSessionInfo) -> {
|
||||
dtlsSessionInfo.setLastActivityTime(System.currentTimeMillis());
|
||||
return dtlsSessionInfo;
|
||||
});
|
||||
if (tbCoapDtlsSessionInfo != null) {
|
||||
processRequest(exchange, type, request, tbCoapDtlsSessionInfo.getMsg(), tbCoapDtlsSessionInfo.getDeviceProfile());
|
||||
} else {
|
||||
processAccessTokenRequest(exchange, type, request);
|
||||
}
|
||||
} else {
|
||||
processAccessTokenRequest(exchange, type, request);
|
||||
}
|
||||
}
|
||||
|
||||
private void processAccessTokenRequest(CoapExchange exchange, SessionMsgType type, Request request) {
|
||||
Optional<DeviceTokenCredentials> credentials = decodeCredentials(request);
|
||||
if (credentials.isEmpty()) {
|
||||
exchange.respond(CoAP.ResponseCode.UNAUTHORIZED);
|
||||
return;
|
||||
}
|
||||
transportService.process(DeviceTransportType.COAP, TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(credentials.get().getCredentialsId()).build(),
|
||||
new CoapDeviceAuthCallback(exchange, (deviceCredentials, deviceProfile) -> processRequest(exchange, type, request, deviceCredentials, deviceProfile)));
|
||||
}
|
||||
|
||||
private void processRequest(CoapExchange exchange, SessionMsgType type, Request request, ValidateDeviceCredentialsResponse deviceCredentials, DeviceProfile deviceProfile) {
|
||||
TbCoapClientState clientState = null;
|
||||
try {
|
||||
clientState = clients.getOrCreateClient(type, deviceCredentials, deviceProfile);
|
||||
clients.awake(clientState);
|
||||
switch (type) {
|
||||
case POST_ATTRIBUTES_REQUEST:
|
||||
handlePostAttributesRequest(clientState, exchange, request);
|
||||
break;
|
||||
case POST_TELEMETRY_REQUEST:
|
||||
handlePostTelemetryRequest(clientState, exchange, request);
|
||||
break;
|
||||
case CLAIM_REQUEST:
|
||||
handleClaimRequest(clientState, exchange, request);
|
||||
break;
|
||||
case SUBSCRIBE_ATTRIBUTES_REQUEST:
|
||||
handleAttributeSubscribeRequest(clientState, exchange, request);
|
||||
break;
|
||||
case UNSUBSCRIBE_ATTRIBUTES_REQUEST:
|
||||
handleAttributeUnsubscribeRequest(clientState, exchange, request);
|
||||
break;
|
||||
case SUBSCRIBE_RPC_COMMANDS_REQUEST:
|
||||
handleRpcSubscribeRequest(clientState, exchange, request);
|
||||
break;
|
||||
case UNSUBSCRIBE_RPC_COMMANDS_REQUEST:
|
||||
handleRpcUnsubscribeRequest(clientState, exchange, request);
|
||||
break;
|
||||
case TO_DEVICE_RPC_RESPONSE:
|
||||
handleToDeviceRpcResponse(clientState, exchange, request);
|
||||
break;
|
||||
case TO_SERVER_RPC_REQUEST:
|
||||
handleToServerRpcRequest(clientState, exchange, request);
|
||||
break;
|
||||
case GET_ATTRIBUTES_REQUEST:
|
||||
handleGetAttributesRequest(clientState, exchange, request);
|
||||
break;
|
||||
}
|
||||
} catch (AdaptorException e) {
|
||||
if (clientState != null) {
|
||||
log.trace("[{}] Failed to decode message: ", clientState.getDeviceId(), e);
|
||||
}
|
||||
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePostAttributesRequest(TbCoapClientState clientState, CoapExchange exchange, Request request) throws AdaptorException {
|
||||
TransportProtos.SessionInfoProto sessionInfo = clients.getNewSyncSession(clientState);
|
||||
UUID sessionId = toSessionId(sessionInfo);
|
||||
transportService.process(sessionInfo, clientState.getAdaptor().convertToPostAttributes(sessionId, request,
|
||||
clientState.getConfiguration().getAttributesMsgDescriptor()),
|
||||
new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
|
||||
private void handlePostTelemetryRequest(TbCoapClientState clientState, CoapExchange exchange, Request request) throws AdaptorException {
|
||||
TransportProtos.SessionInfoProto sessionInfo = clients.getNewSyncSession(clientState);
|
||||
UUID sessionId = toSessionId(sessionInfo);
|
||||
transportService.process(sessionInfo, clientState.getAdaptor().convertToPostTelemetry(sessionId, request,
|
||||
clientState.getConfiguration().getTelemetryMsgDescriptor()),
|
||||
new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
|
||||
private void handleClaimRequest(TbCoapClientState clientState, CoapExchange exchange, Request request) throws AdaptorException {
|
||||
TransportProtos.SessionInfoProto sessionInfo = clients.getNewSyncSession(clientState);
|
||||
UUID sessionId = toSessionId(sessionInfo);
|
||||
transportService.process(sessionInfo,
|
||||
clientState.getAdaptor().convertToClaimDevice(sessionId, request, sessionInfo),
|
||||
new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
|
||||
private void handleAttributeSubscribeRequest(TbCoapClientState clientState, CoapExchange exchange, Request request) {
|
||||
String attrSubToken = getTokenFromRequest(request);
|
||||
if (!clients.registerAttributeObservation(clientState, attrSubToken, exchange)) {
|
||||
log.warn("[{}] Received duplicate attribute subscribe request for token: {}", clientState.getDeviceId(), attrSubToken);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleAttributeUnsubscribeRequest(TbCoapClientState clientState, CoapExchange exchange, Request request) {
|
||||
clients.deregisterAttributeObservation(clientState, getTokenFromRequest(request), exchange);
|
||||
}
|
||||
|
||||
private void handleRpcUnsubscribeRequest(TbCoapClientState clientState, CoapExchange exchange, Request request) {
|
||||
clients.deregisterRpcObservation(clientState, getTokenFromRequest(request), exchange);
|
||||
}
|
||||
|
||||
private void handleToDeviceRpcResponse(TbCoapClientState clientState, CoapExchange exchange, Request request) throws AdaptorException {
|
||||
TransportProtos.SessionInfoProto session = clientState.getSession();
|
||||
if (session == null) {
|
||||
session = clients.getNewSyncSession(clientState);
|
||||
}
|
||||
UUID sessionId = toSessionId(session);
|
||||
transportService.process(session,
|
||||
clientState.getAdaptor().convertToDeviceRpcResponse(sessionId, request, clientState.getConfiguration().getRpcResponseMsgDescriptor()),
|
||||
new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
|
||||
private void handleRpcSubscribeRequest(TbCoapClientState clientState, CoapExchange exchange, Request request) {
|
||||
String rpcSubToken = getTokenFromRequest(request);
|
||||
if (!clients.registerRpcObservation(clientState, rpcSubToken, exchange)) {
|
||||
log.warn("[{}] Received duplicate rpc subscribe request.", rpcSubToken);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleGetAttributesRequest(TbCoapClientState clientState, CoapExchange exchange, Request request) throws AdaptorException {
|
||||
TransportProtos.SessionInfoProto sessionInfo = clients.getNewSyncSession(clientState);
|
||||
UUID sessionId = toSessionId(sessionInfo);
|
||||
transportService.registerSyncSession(sessionInfo, new GetAttributesSyncSessionCallback(clientState, exchange, request), timeout);
|
||||
transportService.process(sessionInfo,
|
||||
clientState.getAdaptor().convertToGetAttributes(sessionId, request),
|
||||
new CoapNoOpCallback(exchange));
|
||||
}
|
||||
|
||||
private void handleToServerRpcRequest(TbCoapClientState clientState, CoapExchange exchange, Request request) throws AdaptorException {
|
||||
TransportProtos.SessionInfoProto sessionInfo = clients.getNewSyncSession(clientState);
|
||||
UUID sessionId = toSessionId(sessionInfo);
|
||||
transportService.registerSyncSession(sessionInfo, new ToServerRpcSyncSessionCallback(clientState, exchange, request), timeout);
|
||||
transportService.process(sessionInfo,
|
||||
clientState.getAdaptor().convertToServerRpcRequest(sessionId, request),
|
||||
new CoapNoOpCallback(exchange));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an empty ACK if we are unable to send the full response within the timeout.
|
||||
* If the full response is transmitted before the timeout this will not do anything.
|
||||
* If this is triggered the full response will be sent in a separate CON/NON message.
|
||||
* Essentially this allows the use of piggybacked responses.
|
||||
*/
|
||||
private void deferAccept(CoapExchange exchange) {
|
||||
if (piggybackTimeout > 0) {
|
||||
transportContext.getScheduler().schedule(exchange::accept, piggybackTimeout, TimeUnit.MILLISECONDS);
|
||||
} else {
|
||||
exchange.accept();
|
||||
}
|
||||
}
|
||||
|
||||
private UUID toSessionId(TransportProtos.SessionInfoProto sessionInfoProto) {
|
||||
return new UUID(sessionInfoProto.getSessionIdMSB(), sessionInfoProto.getSessionIdLSB());
|
||||
}
|
||||
|
||||
private String getTokenFromRequest(Request request) {
|
||||
return (request.getSourceContext() != null ? request.getSourceContext().getPeerAddress().getAddress().getHostAddress() : "null")
|
||||
+ ":" + (request.getSourceContext() != null ? request.getSourceContext().getPeerAddress().getPort() : -1) + ":" + request.getTokenString();
|
||||
}
|
||||
|
||||
private Optional<DeviceTokenCredentials> decodeCredentials(Request request) {
|
||||
List<String> uriPath = request.getOptions().getUriPath();
|
||||
if (uriPath.size() > ACCESS_TOKEN_POSITION) {
|
||||
return Optional.of(new DeviceTokenCredentials(uriPath.get(ACCESS_TOKEN_POSITION - 1)));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<FeatureType> getFeatureType(Request request) {
|
||||
List<String> uriPath = request.getOptions().getUriPath();
|
||||
try {
|
||||
if (uriPath.size() >= FEATURE_TYPE_POSITION) {
|
||||
return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION - 1).toUpperCase()));
|
||||
} else if (uriPath.size() >= FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST) {
|
||||
if (uriPath.contains(DataConstants.PROVISION)) {
|
||||
return Optional.of(FeatureType.valueOf(DataConstants.PROVISION.toUpperCase()));
|
||||
}
|
||||
return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST - 1).toUpperCase()));
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
log.warn("Failed to decode feature type: {}", uriPath);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public static Optional<Integer> getRequestId(Request request) {
|
||||
List<String> uriPath = request.getOptions().getUriPath();
|
||||
try {
|
||||
if (uriPath.size() >= REQUEST_ID_POSITION) {
|
||||
return Optional.of(Integer.valueOf(uriPath.get(REQUEST_ID_POSITION - 1)));
|
||||
} else {
|
||||
return Optional.of(Integer.valueOf(uriPath.get(REQUEST_ID_POSITION_CERTIFICATE_REQUEST - 1)));
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
log.warn("Failed to decode feature type: {}", uriPath);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource getChild(String name) {
|
||||
return this;
|
||||
}
|
||||
|
||||
private static class DeviceProvisionCallback implements TransportServiceCallback<TransportProtos.ProvisionDeviceResponseMsg> {
|
||||
private final CoapExchange exchange;
|
||||
private final TransportPayloadType payloadType;
|
||||
|
||||
DeviceProvisionCallback(CoapExchange exchange, TransportPayloadType payloadType) {
|
||||
this.exchange = exchange;
|
||||
this.payloadType = payloadType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(TransportProtos.ProvisionDeviceResponseMsg msg) {
|
||||
CoAP.ResponseCode responseCode = CoAP.ResponseCode.CREATED;
|
||||
if (!msg.getStatus().equals(TransportProtos.ResponseStatus.SUCCESS)) {
|
||||
responseCode = CoAP.ResponseCode.BAD_REQUEST;
|
||||
}
|
||||
if (payloadType.equals(TransportPayloadType.JSON)) {
|
||||
exchange.respond(responseCode, JsonConverter.toJson(msg).toString());
|
||||
} else {
|
||||
exchange.respond(responseCode, msg.toByteArray());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
log.warn("Failed to process request", e);
|
||||
exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
public class CoapResourceObserver implements ResourceObserver {
|
||||
|
||||
@Override
|
||||
public void changedName(String old) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedPath(String old) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addedChild(Resource child) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removedChild(Resource child) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addedObserveRelation(ObserveRelation relation) {
|
||||
Request request = relation.getExchange().getRequest();
|
||||
String token = getTokenFromRequest(request);
|
||||
clients.registerObserveRelation(token, relation);
|
||||
log.trace("Added Observe relation for token: {}", token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removedObserveRelation(ObserveRelation relation) {
|
||||
Request request = relation.getExchange().getRequest();
|
||||
String token = getTokenFromRequest(request);
|
||||
clients.deregisterObserveRelation(token);
|
||||
log.trace("Relation removed for token: {}", token);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.CoapResource;
|
||||
import org.eclipse.californium.core.CoapServer;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.thingsboard.server.coapserver.CoapServerService;
|
||||
import org.thingsboard.server.coapserver.TbCoapServerComponent;
|
||||
import org.thingsboard.server.common.data.DataConstants;
|
||||
import org.thingsboard.server.common.data.TbTransportService;
|
||||
import org.thingsboard.server.common.data.ota.OtaPackageType;
|
||||
import org.thingsboard.server.transport.coap.efento.CoapEfentoTransportResource;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
@Service("CoapTransportService")
|
||||
@TbCoapServerComponent
|
||||
@Slf4j
|
||||
public class CoapTransportService implements TbTransportService {
|
||||
|
||||
private static final String V1 = "v1";
|
||||
private static final String API = "api";
|
||||
private static final String EFENTO = "efento";
|
||||
public static final String MEASUREMENTS = "m";
|
||||
public static final String DEVICE_INFO = "i";
|
||||
public static final String CONFIGURATION = "c";
|
||||
public static final String CURRENT_TIMESTAMP = "t";
|
||||
|
||||
@Autowired
|
||||
private CoapServerService coapServerService;
|
||||
|
||||
@Autowired
|
||||
private CoapTransportContext coapTransportContext;
|
||||
|
||||
private CoapServer coapServer;
|
||||
|
||||
@PostConstruct
|
||||
public void init() throws UnknownHostException {
|
||||
log.info("Starting CoAP transport...");
|
||||
coapServer = coapServerService.getCoapServer();
|
||||
CoapResource api = new CoapResource(API);
|
||||
api.add(new CoapTransportResource(coapTransportContext, coapServerService, V1));
|
||||
|
||||
CoapEfentoTransportResource efento = new CoapEfentoTransportResource(coapTransportContext, EFENTO);
|
||||
efento.add(new CoapResource(MEASUREMENTS));
|
||||
efento.add(new CoapResource(DEVICE_INFO));
|
||||
efento.add(new CoapResource(CONFIGURATION));
|
||||
efento.add(new CoapResource(CURRENT_TIMESTAMP));
|
||||
coapServer.add(api);
|
||||
coapServer.add(efento);
|
||||
coapServer.add(new OtaPackageTransportResource(coapTransportContext, OtaPackageType.FIRMWARE));
|
||||
coapServer.add(new OtaPackageTransportResource(coapTransportContext, OtaPackageType.SOFTWARE));
|
||||
log.info("CoAP transport started!");
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
log.info("CoAP transport stopped!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return DataConstants.COAP_TRANSPORT_NAME;
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.coap.CoAP;
|
||||
import org.eclipse.californium.core.coap.Request;
|
||||
import org.eclipse.californium.core.coap.Response;
|
||||
import org.eclipse.californium.core.network.Exchange;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
import org.eclipse.californium.core.server.resources.Resource;
|
||||
import org.thingsboard.server.common.data.DeviceTransportType;
|
||||
import org.thingsboard.server.common.data.StringUtils;
|
||||
import org.thingsboard.server.common.data.id.DeviceId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.ota.OtaPackageType;
|
||||
import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
|
||||
import org.thingsboard.server.common.transport.TransportServiceCallback;
|
||||
import org.thingsboard.server.common.transport.auth.SessionInfoCreator;
|
||||
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||
import org.thingsboard.server.transport.coap.callback.CoapDeviceAuthCallback;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
public class OtaPackageTransportResource extends AbstractCoapTransportResource {
|
||||
private static final int ACCESS_TOKEN_POSITION = 2;
|
||||
|
||||
private final OtaPackageType otaPackageType;
|
||||
|
||||
public OtaPackageTransportResource(CoapTransportContext ctx, OtaPackageType otaPackageType) {
|
||||
super(ctx, otaPackageType.getKeyPrefix());
|
||||
this.otaPackageType = otaPackageType;
|
||||
|
||||
this.setObservable(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processHandleGet(CoapExchange exchange) {
|
||||
log.trace("Processing {}", exchange.advanced().getRequest());
|
||||
exchange.accept();
|
||||
Exchange advanced = exchange.advanced();
|
||||
Request request = advanced.getRequest();
|
||||
processAccessTokenRequest(exchange, request);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processHandlePost(CoapExchange exchange) {
|
||||
exchange.respond(CoAP.ResponseCode.METHOD_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
private void processAccessTokenRequest(CoapExchange exchange, Request request) {
|
||||
Optional<DeviceTokenCredentials> credentials = decodeCredentials(request);
|
||||
if (credentials.isEmpty()) {
|
||||
exchange.respond(CoAP.ResponseCode.UNAUTHORIZED);
|
||||
return;
|
||||
}
|
||||
transportService.process(DeviceTransportType.COAP, TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(credentials.get().getCredentialsId()).build(),
|
||||
new CoapDeviceAuthCallback(exchange, (msg, deviceProfile) -> {
|
||||
getOtaPackageCallback(msg, exchange, otaPackageType);
|
||||
}));
|
||||
}
|
||||
|
||||
private void getOtaPackageCallback(ValidateDeviceCredentialsResponse msg, CoapExchange exchange, OtaPackageType firmwareType) {
|
||||
TenantId tenantId = msg.getDeviceInfo().getTenantId();
|
||||
DeviceId deviceId = msg.getDeviceInfo().getDeviceId();
|
||||
TransportProtos.GetOtaPackageRequestMsg requestMsg = TransportProtos.GetOtaPackageRequestMsg.newBuilder()
|
||||
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
|
||||
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
|
||||
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
|
||||
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
|
||||
.setType(firmwareType.name()).build();
|
||||
transportContext.getTransportService().process(SessionInfoCreator.create(msg, transportContext, UUID.randomUUID()), requestMsg, new OtaPackageCallback(exchange));
|
||||
}
|
||||
|
||||
private Optional<DeviceTokenCredentials> decodeCredentials(Request request) {
|
||||
List<String> uriPath = request.getOptions().getUriPath();
|
||||
if (uriPath.size() == ACCESS_TOKEN_POSITION) {
|
||||
return Optional.of(new DeviceTokenCredentials(uriPath.get(ACCESS_TOKEN_POSITION - 1)));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource getChild(String name) {
|
||||
return this;
|
||||
}
|
||||
|
||||
private class OtaPackageCallback implements TransportServiceCallback<TransportProtos.GetOtaPackageResponseMsg> {
|
||||
private final CoapExchange exchange;
|
||||
|
||||
OtaPackageCallback(CoapExchange exchange) {
|
||||
this.exchange = exchange;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(TransportProtos.GetOtaPackageResponseMsg msg) {
|
||||
String title = exchange.getQueryParameter("title");
|
||||
String version = exchange.getQueryParameter("version");
|
||||
if (msg.getResponseStatus().equals(TransportProtos.ResponseStatus.SUCCESS)) {
|
||||
String firmwareId = new UUID(msg.getOtaPackageIdMSB(), msg.getOtaPackageIdLSB()).toString();
|
||||
if ((title == null || msg.getTitle().equals(title)) && (version == null || msg.getVersion().equals(version))) {
|
||||
String strChunkSize = exchange.getQueryParameter("size");
|
||||
String strChunk = exchange.getQueryParameter("chunk");
|
||||
int chunkSize = StringUtils.isEmpty(strChunkSize) ? 0 : Integer.parseInt(strChunkSize);
|
||||
int chunk = StringUtils.isEmpty(strChunk) ? 0 : Integer.parseInt(strChunk);
|
||||
respondOtaPackage(exchange, transportContext.getOtaPackageDataCache().get(firmwareId, chunkSize, chunk));
|
||||
} else {
|
||||
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
|
||||
}
|
||||
} else {
|
||||
exchange.respond(CoAP.ResponseCode.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
log.warn("Failed to process request", e);
|
||||
exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void respondOtaPackage(CoapExchange exchange, byte[] data) {
|
||||
Response response = new Response(CoAP.ResponseCode.CONTENT);
|
||||
if (data != null && data.length > 0) {
|
||||
response.setPayload(data);
|
||||
if (exchange.getRequestOptions().getBlock2() != null) {
|
||||
int chunkSize = exchange.getRequestOptions().getBlock2().getSzx();
|
||||
boolean lastFlag = data.length <= chunkSize;
|
||||
response.getOptions().setBlock2(chunkSize, lastFlag, 0);
|
||||
}
|
||||
transportContext.getExecutor().submit(() -> exchange.respond(response));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.eclipse.californium.core.coap.MessageObserver;
|
||||
import org.eclipse.californium.core.coap.Response;
|
||||
import org.eclipse.californium.elements.EndpointContext;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class TbCoapMessageObserver implements MessageObserver {
|
||||
|
||||
private final int msgId;
|
||||
private final Consumer<Integer> onAcknowledge;
|
||||
private final Consumer<Integer> onTimeout;
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRetransmission() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Response response) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAcknowledgement() {
|
||||
onAcknowledge.accept(msgId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReject() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimeout() {
|
||||
if (onTimeout != null) {
|
||||
onTimeout.accept(msgId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReadyToSend() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnecting() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDtlsRetransmission(int flight) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSent(boolean retransmission) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendError(Throwable error) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponseHandlingError(Throwable cause) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContextEstablished(EndpointContext endpointContext) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransferComplete() {
|
||||
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap;
|
||||
|
||||
import com.google.protobuf.Descriptors;
|
||||
import com.google.protobuf.DynamicMessage;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class TransportConfigurationContainer {
|
||||
|
||||
private boolean jsonPayload;
|
||||
private Descriptors.Descriptor telemetryMsgDescriptor;
|
||||
private Descriptors.Descriptor attributesMsgDescriptor;
|
||||
private Descriptors.Descriptor rpcResponseMsgDescriptor;
|
||||
private DynamicMessage.Builder rpcRequestDynamicMessageBuilder;
|
||||
|
||||
public TransportConfigurationContainer(boolean jsonPayload, Descriptors.Descriptor telemetryMsgDescriptor, Descriptors.Descriptor attributesMsgDescriptor, Descriptors.Descriptor rpcResponseMsgDescriptor, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) {
|
||||
this.jsonPayload = jsonPayload;
|
||||
this.telemetryMsgDescriptor = telemetryMsgDescriptor;
|
||||
this.attributesMsgDescriptor = attributesMsgDescriptor;
|
||||
this.rpcResponseMsgDescriptor = rpcResponseMsgDescriptor;
|
||||
this.rpcRequestDynamicMessageBuilder = rpcRequestDynamicMessageBuilder;
|
||||
}
|
||||
|
||||
public TransportConfigurationContainer(boolean jsonPayload) {
|
||||
this.jsonPayload = jsonPayload;
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.adaptors;
|
||||
|
||||
import org.eclipse.californium.core.coap.Request;
|
||||
import org.thingsboard.server.common.data.StringUtils;
|
||||
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class CoapAdaptorUtils {
|
||||
|
||||
public static TransportProtos.GetAttributeRequestMsg toGetAttributeRequestMsg(Request inbound) throws AdaptorException {
|
||||
List<String> queryElements = inbound.getOptions().getUriQuery();
|
||||
TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder();
|
||||
if (queryElements != null && queryElements.size() > 0) {
|
||||
Set<String> clientKeys = toKeys(queryElements, "clientKeys");
|
||||
Set<String> sharedKeys = toKeys(queryElements, "sharedKeys");
|
||||
if (clientKeys != null) {
|
||||
result.addAllClientAttributeNames(clientKeys);
|
||||
}
|
||||
if (sharedKeys != null) {
|
||||
result.addAllSharedAttributeNames(sharedKeys);
|
||||
}
|
||||
}
|
||||
result.setOnlyShared(false);
|
||||
return result.build();
|
||||
}
|
||||
|
||||
private static Set<String> toKeys(List<String> queryElements, String attributeName) throws AdaptorException {
|
||||
String keys = null;
|
||||
for (String queryElement : queryElements) {
|
||||
String[] queryItem = queryElement.split("=");
|
||||
if (queryItem.length == 2 && queryItem[0].equals(attributeName)) {
|
||||
keys = queryItem[1];
|
||||
}
|
||||
}
|
||||
if (keys != null && !StringUtils.isEmpty(keys)) {
|
||||
return new HashSet<>(Arrays.asList(keys.split(",")));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.adaptors;
|
||||
|
||||
import com.google.protobuf.Descriptors;
|
||||
import com.google.protobuf.DynamicMessage;
|
||||
import org.eclipse.californium.core.coap.Request;
|
||||
import org.eclipse.californium.core.coap.Response;
|
||||
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface CoapTransportAdaptor {
|
||||
|
||||
TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, Request inbound, Descriptors.Descriptor telemetryMsgDescriptor) throws AdaptorException;
|
||||
|
||||
TransportProtos.PostAttributeMsg convertToPostAttributes(UUID sessionId, Request inbound, Descriptors.Descriptor attributesMsgDescriptor) throws AdaptorException;
|
||||
|
||||
TransportProtos.GetAttributeRequestMsg convertToGetAttributes(UUID sessionId, Request inbound) throws AdaptorException;
|
||||
|
||||
TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound, Descriptors.Descriptor rpcResponseMsgDescriptor) throws AdaptorException;
|
||||
|
||||
TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(UUID sessionId, Request inbound) throws AdaptorException;
|
||||
|
||||
TransportProtos.ClaimDeviceMsg convertToClaimDevice(UUID sessionId, Request inbound, TransportProtos.SessionInfoProto sessionInfo) throws AdaptorException;
|
||||
|
||||
Response convertToPublish(TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException;
|
||||
|
||||
Response convertToPublish(TransportProtos.AttributeUpdateNotificationMsg notificationMsg) throws AdaptorException;
|
||||
|
||||
Response convertToPublish(TransportProtos.ToDeviceRpcRequestMsg rpcRequest, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) throws AdaptorException;
|
||||
|
||||
Response convertToPublish(TransportProtos.ToServerRpcResponseMsg msg) throws AdaptorException;
|
||||
|
||||
ProvisionDeviceRequestMsg convertToProvisionRequestMsg(UUID sessionId, Request inbound) throws AdaptorException;
|
||||
|
||||
int getContentFormat();
|
||||
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.adaptors;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.protobuf.Descriptors;
|
||||
import com.google.protobuf.DynamicMessage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.coap.CoAP;
|
||||
import org.eclipse.californium.core.coap.MediaTypeRegistry;
|
||||
import org.eclipse.californium.core.coap.Request;
|
||||
import org.eclipse.californium.core.coap.Response;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.thingsboard.server.common.data.StringUtils;
|
||||
import org.thingsboard.server.common.data.id.DeviceId;
|
||||
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
|
||||
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||
import org.thingsboard.server.transport.coap.CoapTransportResource;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class JsonCoapAdaptor implements CoapTransportAdaptor {
|
||||
|
||||
@Override
|
||||
public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, Request inbound, Descriptors.Descriptor telemetryMsgDescriptor) throws AdaptorException {
|
||||
String payload = validatePayload(sessionId, inbound, false);
|
||||
try {
|
||||
return JsonConverter.convertToTelemetryProto(new JsonParser().parse(payload));
|
||||
} catch (IllegalStateException | JsonSyntaxException ex) {
|
||||
throw new AdaptorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportProtos.PostAttributeMsg convertToPostAttributes(UUID sessionId, Request inbound, Descriptors.Descriptor attributesMsgDescriptor) throws AdaptorException {
|
||||
String payload = validatePayload(sessionId, inbound, false);
|
||||
try {
|
||||
return JsonConverter.convertToAttributesProto(new JsonParser().parse(payload));
|
||||
} catch (IllegalStateException | JsonSyntaxException ex) {
|
||||
throw new AdaptorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportProtos.GetAttributeRequestMsg convertToGetAttributes(UUID sessionId, Request inbound) throws AdaptorException {
|
||||
return CoapAdaptorUtils.toGetAttributeRequestMsg(inbound);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound, Descriptors.Descriptor rpcResponseMsgDescriptor) throws AdaptorException {
|
||||
Optional<Integer> requestId = CoapTransportResource.getRequestId(inbound);
|
||||
String payload = validatePayload(sessionId, inbound, false);
|
||||
JsonObject response = new JsonParser().parse(payload).getAsJsonObject();
|
||||
return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId.orElseThrow(() -> new AdaptorException("Request id is missing!")))
|
||||
.setPayload(response.toString()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(UUID sessionId, Request inbound) throws AdaptorException {
|
||||
String payload = validatePayload(sessionId, inbound, false);
|
||||
return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportProtos.ClaimDeviceMsg convertToClaimDevice(UUID sessionId, Request inbound, TransportProtos.SessionInfoProto sessionInfo) throws AdaptorException {
|
||||
DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB()));
|
||||
String payload = validatePayload(sessionId, inbound, true);
|
||||
try {
|
||||
return JsonConverter.convertToClaimDeviceProto(deviceId, payload);
|
||||
} catch (IllegalStateException | JsonSyntaxException ex) {
|
||||
throw new AdaptorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response convertToPublish(TransportProtos.AttributeUpdateNotificationMsg msg) throws AdaptorException {
|
||||
return getObserveNotification(JsonConverter.toJson(msg));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response convertToPublish(TransportProtos.ToDeviceRpcRequestMsg msg, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) throws AdaptorException {
|
||||
return getObserveNotification(JsonConverter.toJson(msg, true));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response convertToPublish(TransportProtos.ToServerRpcResponseMsg msg) throws AdaptorException {
|
||||
Response response = new Response(CoAP.ResponseCode.CONTENT);
|
||||
JsonElement result = JsonConverter.toJson(msg);
|
||||
response.setPayload(result.toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(UUID sessionId, Request inbound) throws AdaptorException {
|
||||
String payload = validatePayload(sessionId, inbound, false);
|
||||
try {
|
||||
return JsonConverter.convertToProvisionRequestMsg(payload);
|
||||
} catch (IllegalStateException | JsonSyntaxException ex) {
|
||||
throw new AdaptorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response convertToPublish(TransportProtos.GetAttributeResponseMsg msg) throws AdaptorException {
|
||||
if (msg.getSharedStateMsg()) {
|
||||
if (StringUtils.isEmpty(msg.getError())) {
|
||||
Response response = new Response(CoAP.ResponseCode.CONTENT);
|
||||
TransportProtos.AttributeUpdateNotificationMsg notificationMsg = TransportProtos.AttributeUpdateNotificationMsg.newBuilder().addAllSharedUpdated(msg.getSharedAttributeListList()).build();
|
||||
JsonObject result = JsonConverter.toJson(notificationMsg);
|
||||
response.setPayload(result.toString());
|
||||
return response;
|
||||
} else {
|
||||
return new Response(CoAP.ResponseCode.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
} else {
|
||||
if (msg.getClientAttributeListCount() == 0 && msg.getSharedAttributeListCount() == 0) {
|
||||
return new Response(CoAP.ResponseCode.NOT_FOUND);
|
||||
} else {
|
||||
Response response = new Response(CoAP.ResponseCode.CONTENT);
|
||||
JsonObject result = JsonConverter.toJson(msg);
|
||||
response.setPayload(result.toString());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Response getObserveNotification(JsonElement json) {
|
||||
Response response = new Response(CoAP.ResponseCode.CONTENT);
|
||||
response.setPayload(json.toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
private String validatePayload(UUID sessionId, Request inbound, boolean isEmptyPayloadAllowed) throws AdaptorException {
|
||||
String payload = inbound.getPayloadString();
|
||||
if (payload == null) {
|
||||
log.debug("[{}] Payload is empty!", sessionId);
|
||||
if (!isEmptyPayloadAllowed) {
|
||||
throw new AdaptorException(new IllegalArgumentException("Payload is empty!"));
|
||||
}
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getContentFormat() {
|
||||
return MediaTypeRegistry.APPLICATION_JSON;
|
||||
}
|
||||
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.adaptors;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.protobuf.Descriptors;
|
||||
import com.google.protobuf.DynamicMessage;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.coap.CoAP;
|
||||
import org.eclipse.californium.core.coap.MediaTypeRegistry;
|
||||
import org.eclipse.californium.core.coap.Request;
|
||||
import org.eclipse.californium.core.coap.Response;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.thingsboard.server.common.data.StringUtils;
|
||||
import org.thingsboard.server.common.data.id.DeviceId;
|
||||
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
|
||||
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
|
||||
import org.thingsboard.server.common.transport.adaptor.ProtoConverter;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||
import org.thingsboard.server.transport.coap.CoapTransportResource;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class ProtoCoapAdaptor implements CoapTransportAdaptor {
|
||||
|
||||
@Override
|
||||
public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, Request inbound, Descriptors.Descriptor telemetryMsgDescriptor) throws AdaptorException {
|
||||
ProtoConverter.validateDescriptor(telemetryMsgDescriptor);
|
||||
try {
|
||||
return JsonConverter.convertToTelemetryProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), telemetryMsgDescriptor)));
|
||||
} catch (Exception e) {
|
||||
throw new AdaptorException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportProtos.PostAttributeMsg convertToPostAttributes(UUID sessionId, Request inbound, Descriptors.Descriptor attributesMsgDescriptor) throws AdaptorException {
|
||||
ProtoConverter.validateDescriptor(attributesMsgDescriptor);
|
||||
try {
|
||||
return JsonConverter.convertToAttributesProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), attributesMsgDescriptor)));
|
||||
} catch (Exception e) {
|
||||
throw new AdaptorException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportProtos.GetAttributeRequestMsg convertToGetAttributes(UUID sessionId, Request inbound) throws AdaptorException {
|
||||
return CoapAdaptorUtils.toGetAttributeRequestMsg(inbound);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound, Descriptors.Descriptor rpcResponseMsgDescriptor) throws AdaptorException {
|
||||
Optional<Integer> requestId = CoapTransportResource.getRequestId(inbound);
|
||||
if (requestId.isEmpty()) {
|
||||
throw new AdaptorException("Request id is missing!");
|
||||
} else {
|
||||
ProtoConverter.validateDescriptor(rpcResponseMsgDescriptor);
|
||||
try {
|
||||
JsonElement response = new JsonParser().parse(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), rpcResponseMsgDescriptor));
|
||||
return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId.orElseThrow(() -> new AdaptorException("Request id is missing!")))
|
||||
.setPayload(response.toString()).build();
|
||||
} catch (Exception e) {
|
||||
throw new AdaptorException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(UUID sessionId, Request inbound) throws AdaptorException {
|
||||
try {
|
||||
return ProtoConverter.convertToServerRpcRequest(inbound.getPayload(), 0);
|
||||
} catch (InvalidProtocolBufferException ex) {
|
||||
throw new AdaptorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportProtos.ClaimDeviceMsg convertToClaimDevice(UUID sessionId, Request inbound, TransportProtos.SessionInfoProto sessionInfo) throws AdaptorException {
|
||||
DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB()));
|
||||
try {
|
||||
return ProtoConverter.convertToClaimDeviceProto(deviceId, inbound.getPayload());
|
||||
} catch (InvalidProtocolBufferException ex) {
|
||||
throw new AdaptorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(UUID sessionId, Request inbound) throws AdaptorException {
|
||||
try {
|
||||
return ProtoConverter.convertToProvisionRequestMsg(inbound.getPayload());
|
||||
} catch (InvalidProtocolBufferException ex) {
|
||||
throw new AdaptorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response convertToPublish(TransportProtos.AttributeUpdateNotificationMsg msg) throws AdaptorException {
|
||||
return getObserveNotification(msg.toByteArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response convertToPublish(TransportProtos.ToDeviceRpcRequestMsg rpcRequest, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) throws AdaptorException {
|
||||
return getObserveNotification(ProtoConverter.convertToRpcRequest(rpcRequest, rpcRequestDynamicMessageBuilder));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response convertToPublish(TransportProtos.ToServerRpcResponseMsg msg) throws AdaptorException {
|
||||
Response response = new Response(CoAP.ResponseCode.CONTENT);
|
||||
response.setPayload(msg.toByteArray());
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response convertToPublish(TransportProtos.GetAttributeResponseMsg msg) throws AdaptorException {
|
||||
if (msg.getSharedStateMsg()) {
|
||||
if (StringUtils.isEmpty(msg.getError())) {
|
||||
Response response = new Response(CoAP.ResponseCode.CONTENT);
|
||||
TransportProtos.AttributeUpdateNotificationMsg notificationMsg = TransportProtos.AttributeUpdateNotificationMsg.newBuilder().addAllSharedUpdated(msg.getSharedAttributeListList()).build();
|
||||
response.setPayload(notificationMsg.toByteArray());
|
||||
return response;
|
||||
} else {
|
||||
return new Response(CoAP.ResponseCode.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
} else {
|
||||
if (msg.getClientAttributeListCount() == 0 && msg.getSharedAttributeListCount() == 0) {
|
||||
return new Response(CoAP.ResponseCode.NOT_FOUND);
|
||||
} else {
|
||||
Response response = new Response(CoAP.ResponseCode.CONTENT);
|
||||
response.setPayload(msg.toByteArray());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Response getObserveNotification(byte[] notification) {
|
||||
Response response = new Response(CoAP.ResponseCode.CONTENT);
|
||||
response.setPayload(notification);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getContentFormat() {
|
||||
return MediaTypeRegistry.APPLICATION_OCTET_STREAM;
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.callback;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.coap.Request;
|
||||
import org.eclipse.californium.core.coap.Response;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
import org.thingsboard.server.common.data.id.DeviceId;
|
||||
import org.thingsboard.server.common.transport.SessionMsgListener;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||
import org.thingsboard.server.transport.coap.client.TbCoapClientState;
|
||||
import org.thingsboard.server.transport.coap.client.TbCoapContentFormatUtil;
|
||||
import org.thingsboard.server.transport.coap.client.TbCoapObservationState;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public abstract class AbstractSyncSessionCallback implements SessionMsgListener {
|
||||
|
||||
protected final TbCoapClientState state;
|
||||
protected final CoapExchange exchange;
|
||||
protected final Request request;
|
||||
|
||||
@Override
|
||||
public void onGetAttributesResponse(TransportProtos.GetAttributeResponseMsg getAttributesResponse) {
|
||||
logUnsupportedCommandMessage(getAttributesResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttributeUpdate(UUID sessionId, TransportProtos.AttributeUpdateNotificationMsg attributeUpdateNotification) {
|
||||
logUnsupportedCommandMessage(attributeUpdateNotification);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoteSessionCloseCommand(UUID sessionId, TransportProtos.SessionCloseNotificationProto sessionCloseNotification) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeviceDeleted(DeviceId deviceId) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToDeviceRpcRequest(UUID sessionId, TransportProtos.ToDeviceRpcRequestMsg toDeviceRequest) {
|
||||
logUnsupportedCommandMessage(toDeviceRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg toServerResponse) {
|
||||
logUnsupportedCommandMessage(toServerResponse);
|
||||
}
|
||||
|
||||
private void logUnsupportedCommandMessage(Object update) {
|
||||
log.trace("[{}] Ignore unsupported update: {}", state.getDeviceId(), update);
|
||||
}
|
||||
|
||||
public static boolean isConRequest(TbCoapObservationState state) {
|
||||
if (state != null) {
|
||||
return state.getExchange().advanced().getRequest().isConfirmable();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected void respond(Response response) {
|
||||
response.getOptions().setContentFormat(TbCoapContentFormatUtil.getContentFormat(exchange.getRequestOptions().getContentFormat(), state.getContentFormat()));
|
||||
exchange.respond(response);
|
||||
}
|
||||
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.callback;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.coap.CoAP;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
import org.thingsboard.server.common.data.DeviceProfile;
|
||||
import org.thingsboard.server.common.transport.TransportServiceCallback;
|
||||
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
@Slf4j
|
||||
public class CoapDeviceAuthCallback implements TransportServiceCallback<ValidateDeviceCredentialsResponse> {
|
||||
private final CoapExchange exchange;
|
||||
private final BiConsumer<ValidateDeviceCredentialsResponse, DeviceProfile> onSuccess;
|
||||
|
||||
public CoapDeviceAuthCallback(CoapExchange exchange, BiConsumer<ValidateDeviceCredentialsResponse, DeviceProfile> onSuccess) {
|
||||
this.exchange = exchange;
|
||||
this.onSuccess = onSuccess;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(ValidateDeviceCredentialsResponse msg) {
|
||||
DeviceProfile deviceProfile = msg.getDeviceProfile();
|
||||
if (msg.hasDeviceInfo() && deviceProfile != null) {
|
||||
onSuccess.accept(msg, deviceProfile);
|
||||
} else {
|
||||
exchange.respond(CoAP.ResponseCode.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
log.warn("Failed to process request", e);
|
||||
exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.callback;
|
||||
|
||||
import org.eclipse.californium.core.coap.CoAP;
|
||||
import org.eclipse.californium.core.coap.Response;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
import org.thingsboard.server.common.transport.TransportServiceCallback;
|
||||
|
||||
public class CoapEfentoCallback implements TransportServiceCallback<Void> {
|
||||
|
||||
protected final CoapExchange exchange;
|
||||
protected final CoAP.ResponseCode onSuccessResponse;
|
||||
protected final CoAP.ResponseCode onFailureResponse;
|
||||
|
||||
public CoapEfentoCallback(CoapExchange exchange, CoAP.ResponseCode onSuccessResponse, CoAP.ResponseCode onFailureResponse) {
|
||||
this.exchange = exchange;
|
||||
this.onSuccessResponse = onSuccessResponse;
|
||||
this.onFailureResponse = onFailureResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(Void msg) {
|
||||
//We respond only to confirmed requests in order to reduce battery consumption for Efento devices.
|
||||
if (isConRequest()) {
|
||||
exchange.respond(new Response(onSuccessResponse));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
exchange.respond(onFailureResponse);
|
||||
}
|
||||
|
||||
protected boolean isConRequest() {
|
||||
return exchange.advanced().getRequest().isConfirmable();
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.callback;
|
||||
|
||||
import org.eclipse.californium.core.coap.CoAP;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
import org.thingsboard.server.common.transport.TransportServiceCallback;
|
||||
|
||||
public class CoapNoOpCallback implements TransportServiceCallback<Void> {
|
||||
private final CoapExchange exchange;
|
||||
|
||||
public CoapNoOpCallback(CoapExchange exchange) {
|
||||
this.exchange = exchange;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(Void msg) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.callback;
|
||||
|
||||
import org.eclipse.californium.core.coap.CoAP;
|
||||
import org.eclipse.californium.core.coap.Response;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
import org.thingsboard.server.common.transport.TransportServiceCallback;
|
||||
|
||||
public class CoapOkCallback implements TransportServiceCallback<Void> {
|
||||
|
||||
protected final CoapExchange exchange;
|
||||
protected final CoAP.ResponseCode onSuccessResponse;
|
||||
protected final CoAP.ResponseCode onFailureResponse;
|
||||
|
||||
public CoapOkCallback(CoapExchange exchange, CoAP.ResponseCode onSuccessResponse, CoAP.ResponseCode onFailureResponse) {
|
||||
this.exchange = exchange;
|
||||
this.onSuccessResponse = onSuccessResponse;
|
||||
this.onFailureResponse = onFailureResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(Void msg) {
|
||||
exchange.respond(new Response(onSuccessResponse));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
exchange.respond(onFailureResponse);
|
||||
}
|
||||
|
||||
protected boolean isConRequest() {
|
||||
return exchange.advanced().getRequest().isConfirmable();
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.callback;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.coap.CoAP;
|
||||
import org.eclipse.californium.core.coap.Request;
|
||||
import org.eclipse.californium.core.coap.Response;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||
import org.thingsboard.server.transport.coap.client.TbCoapClientState;
|
||||
|
||||
@Slf4j
|
||||
public class GetAttributesSyncSessionCallback extends AbstractSyncSessionCallback {
|
||||
|
||||
public GetAttributesSyncSessionCallback(TbCoapClientState state, CoapExchange exchange, Request request) {
|
||||
super(state, exchange, request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGetAttributesResponse(TransportProtos.GetAttributeResponseMsg msg) {
|
||||
try {
|
||||
respond(state.getAdaptor().convertToPublish(msg));
|
||||
} catch (AdaptorException e) {
|
||||
log.trace("[{}] Failed to reply due to error", state.getDeviceId(), e);
|
||||
exchange.respond(new Response(CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.callback;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.coap.CoAP;
|
||||
import org.eclipse.californium.core.coap.Request;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||
import org.thingsboard.server.transport.coap.client.TbCoapClientState;
|
||||
|
||||
@Slf4j
|
||||
public class ToServerRpcSyncSessionCallback extends AbstractSyncSessionCallback {
|
||||
|
||||
public ToServerRpcSyncSessionCallback(TbCoapClientState state, CoapExchange exchange, Request request) {
|
||||
super(state, exchange, request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg toServerResponse) {
|
||||
try {
|
||||
respond(state.getAdaptor().convertToPublish(toServerResponse));
|
||||
} catch (AdaptorException e) {
|
||||
log.trace("Failed to reply due to error", e);
|
||||
exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.client;
|
||||
|
||||
import org.eclipse.californium.core.observe.ObserveRelation;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
import org.thingsboard.server.common.data.DeviceProfile;
|
||||
import org.thingsboard.server.common.msg.session.SessionMsgType;
|
||||
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
|
||||
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public interface CoapClientContext {
|
||||
|
||||
boolean registerAttributeObservation(TbCoapClientState clientState, String token, CoapExchange exchange);
|
||||
|
||||
boolean registerRpcObservation(TbCoapClientState clientState, String token, CoapExchange exchange);
|
||||
|
||||
AtomicInteger getNotificationCounterByToken(String token);
|
||||
|
||||
TbCoapClientState getOrCreateClient(SessionMsgType type, ValidateDeviceCredentialsResponse deviceCredentials, DeviceProfile deviceProfile) throws AdaptorException;
|
||||
|
||||
TransportProtos.SessionInfoProto getNewSyncSession(TbCoapClientState clientState);
|
||||
|
||||
void deregisterAttributeObservation(TbCoapClientState clientState, String token, CoapExchange exchange);
|
||||
|
||||
void deregisterRpcObservation(TbCoapClientState clientState, String token, CoapExchange exchange);
|
||||
|
||||
void reportActivity();
|
||||
|
||||
void registerObserveRelation(String token, ObserveRelation relation);
|
||||
|
||||
void deregisterObserveRelation(String token);
|
||||
|
||||
boolean awake(TbCoapClientState client);
|
||||
}
|
||||
+840
@@ -0,0 +1,840 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.client;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.coap.CoAP;
|
||||
import org.eclipse.californium.core.coap.Response;
|
||||
import org.eclipse.californium.core.observe.ObserveRelation;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.thingsboard.server.coapserver.CoapServerContext;
|
||||
import org.thingsboard.server.common.data.DataConstants;
|
||||
import org.thingsboard.server.common.data.Device;
|
||||
import org.thingsboard.server.common.data.DeviceProfile;
|
||||
import org.thingsboard.server.common.data.DeviceTransportType;
|
||||
import org.thingsboard.server.common.data.StringUtils;
|
||||
import org.thingsboard.server.common.data.device.data.PowerMode;
|
||||
import org.thingsboard.server.common.data.device.data.PowerSavingConfiguration;
|
||||
import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration;
|
||||
import org.thingsboard.server.common.data.device.profile.CoapDeviceTypeConfiguration;
|
||||
import org.thingsboard.server.common.data.device.profile.DefaultCoapDeviceTypeConfiguration;
|
||||
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
|
||||
import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration;
|
||||
import org.thingsboard.server.common.data.device.profile.JsonTransportPayloadConfiguration;
|
||||
import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration;
|
||||
import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration;
|
||||
import org.thingsboard.server.common.data.id.DeviceId;
|
||||
import org.thingsboard.server.common.data.id.DeviceProfileId;
|
||||
import org.thingsboard.server.common.data.rpc.RpcStatus;
|
||||
import org.thingsboard.server.common.msg.session.FeatureType;
|
||||
import org.thingsboard.server.common.msg.session.SessionMsgType;
|
||||
import org.thingsboard.server.common.transport.DeviceDeletedEvent;
|
||||
import org.thingsboard.server.common.transport.DeviceProfileUpdatedEvent;
|
||||
import org.thingsboard.server.common.transport.DeviceUpdatedEvent;
|
||||
import org.thingsboard.server.common.transport.SessionMsgListener;
|
||||
import org.thingsboard.server.common.transport.TransportDeviceProfileCache;
|
||||
import org.thingsboard.server.common.transport.TransportService;
|
||||
import org.thingsboard.server.common.transport.TransportServiceCallback;
|
||||
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
|
||||
import org.thingsboard.server.common.transport.auth.SessionInfoCreator;
|
||||
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||
import org.thingsboard.server.queue.discovery.PartitionService;
|
||||
import org.thingsboard.server.transport.coap.CoapTransportContext;
|
||||
import org.thingsboard.server.transport.coap.TbCoapMessageObserver;
|
||||
import org.thingsboard.server.transport.coap.TransportConfigurationContainer;
|
||||
import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
|
||||
import org.thingsboard.server.transport.coap.callback.AbstractSyncSessionCallback;
|
||||
import org.thingsboard.server.transport.coap.callback.CoapNoOpCallback;
|
||||
import org.thingsboard.server.transport.coap.callback.CoapOkCallback;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.eclipse.californium.core.coap.Message.MAX_MID;
|
||||
import static org.eclipse.californium.core.coap.Message.NONE;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.coap.enabled}'=='true')")
|
||||
public class DefaultCoapClientContext implements CoapClientContext {
|
||||
|
||||
private final CoapServerContext config;
|
||||
private final CoapTransportContext transportContext;
|
||||
private final TransportService transportService;
|
||||
private final TransportDeviceProfileCache profileCache;
|
||||
private final PartitionService partitionService;
|
||||
private final ConcurrentMap<DeviceId, TbCoapClientState> clients = new ConcurrentHashMap<>();
|
||||
private final ConcurrentMap<String, TbCoapClientState> clientsByToken = new ConcurrentHashMap<>();
|
||||
|
||||
public DefaultCoapClientContext(CoapServerContext config, @Lazy CoapTransportContext transportContext,
|
||||
TransportService transportService, TransportDeviceProfileCache profileCache,
|
||||
PartitionService partitionService) {
|
||||
this.config = config;
|
||||
this.transportContext = transportContext;
|
||||
this.transportService = transportService;
|
||||
this.profileCache = profileCache;
|
||||
this.partitionService = partitionService;
|
||||
}
|
||||
|
||||
@EventListener(DeviceProfileUpdatedEvent.class)
|
||||
public void onApplicationEvent(DeviceProfileUpdatedEvent event) {
|
||||
var deviceProfile = event.getDeviceProfile();
|
||||
clients.values().stream().filter(state -> state.getSession() == null).forEach(state -> {
|
||||
state.lock();
|
||||
try {
|
||||
if (deviceProfile.getId().equals(state.getProfileId())) {
|
||||
initStateAdaptor(deviceProfile, state);
|
||||
}
|
||||
} catch (AdaptorException e) {
|
||||
log.trace("[{}] Failed to update client state due to: ", state.getDeviceId(), e);
|
||||
} finally {
|
||||
state.unlock();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@EventListener(DeviceUpdatedEvent.class)
|
||||
public void onApplicationEvent(DeviceUpdatedEvent event) {
|
||||
var device = event.getDevice();
|
||||
var state = clients.get(device.getId());
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
state.lock();
|
||||
try {
|
||||
if (state.getSession() == null) {
|
||||
clients.remove(device.getId());
|
||||
}
|
||||
} finally {
|
||||
state.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@EventListener(DeviceDeletedEvent.class)
|
||||
public void onApplicationEvent(DeviceDeletedEvent event) {
|
||||
clients.remove(event.getDeviceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean registerAttributeObservation(TbCoapClientState clientState, String token, CoapExchange exchange) {
|
||||
return registerFeatureObservation(clientState, token, exchange, FeatureType.ATTRIBUTES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean registerRpcObservation(TbCoapClientState clientState, String token, CoapExchange exchange) {
|
||||
return registerFeatureObservation(clientState, token, exchange, FeatureType.RPC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AtomicInteger getNotificationCounterByToken(String token) {
|
||||
TbCoapClientState state = clientsByToken.get(token);
|
||||
if (state == null) {
|
||||
log.trace("Failed to find state using token: {}", token);
|
||||
return null;
|
||||
}
|
||||
if (state.getAttrs() != null && state.getAttrs().getToken().equals(token)) {
|
||||
return state.getAttrs().getObserveCounter();
|
||||
} else {
|
||||
log.trace("Failed to find attr subscription using token: {}", token);
|
||||
}
|
||||
if (state.getRpc() != null && state.getRpc().getToken().equals(token)) {
|
||||
return state.getRpc().getObserveCounter();
|
||||
} else {
|
||||
log.trace("Failed to find rpc subscription using token: {}", token);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerObserveRelation(String token, ObserveRelation relation) {
|
||||
TbCoapClientState state = clientsByToken.get(token);
|
||||
if (state == null) {
|
||||
log.trace("Failed to find state using token: {}", token);
|
||||
return;
|
||||
}
|
||||
if (state.getAttrs() != null && state.getAttrs().getToken().equals(token)) {
|
||||
state.getAttrs().setObserveRelation(relation);
|
||||
} else {
|
||||
log.trace("Failed to find attr subscription using token: {}", token);
|
||||
}
|
||||
if (state.getRpc() != null && state.getRpc().getToken().equals(token)) {
|
||||
state.getRpc().setObserveRelation(relation);
|
||||
} else {
|
||||
log.trace("Failed to find rpc subscription using token: {}", token);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deregisterObserveRelation(String token) {
|
||||
TbCoapClientState state = clientsByToken.remove(token);
|
||||
if (state == null) {
|
||||
log.trace("Failed to find state using token: {}", token);
|
||||
return;
|
||||
}
|
||||
if (state.getAttrs() != null && state.getAttrs().getToken().equals(token)) {
|
||||
cancelAttributeSubscription(state);
|
||||
} else {
|
||||
log.trace("Failed to find attr subscription using token: {}", token);
|
||||
}
|
||||
if (state.getRpc() != null && state.getRpc().getToken().equals(token)) {
|
||||
cancelRpcSubscription(state);
|
||||
} else {
|
||||
log.trace("Failed to find rpc subscription using token: {}", token);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportActivity() {
|
||||
for (TbCoapClientState state : clients.values()) {
|
||||
if (state.getSession() != null) {
|
||||
transportService.reportActivity(state.getSession());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onUplink(TbCoapClientState client, boolean notifyOtherServers, long uplinkTs) {
|
||||
PowerMode powerMode = client.getPowerMode();
|
||||
PowerSavingConfiguration profileSettings = null;
|
||||
if (powerMode == null) {
|
||||
var clientProfile = getProfile(client.getProfileId());
|
||||
if (clientProfile.isPresent()) {
|
||||
profileSettings = clientProfile.get().getClientSettings();
|
||||
if (profileSettings != null) {
|
||||
powerMode = profileSettings.getPowerMode();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (powerMode == null || PowerMode.DRX.equals(powerMode)) {
|
||||
client.updateLastUplinkTime(uplinkTs);
|
||||
return;
|
||||
}
|
||||
client.lock();
|
||||
try {
|
||||
long uplinkTime = client.updateLastUplinkTime(uplinkTs);
|
||||
long timeout = getTimeout(client, powerMode, profileSettings);
|
||||
Future<Void> sleepTask = client.getSleepTask();
|
||||
if (sleepTask != null) {
|
||||
sleepTask.cancel(false);
|
||||
}
|
||||
Future<Void> task = transportContext.getScheduler().schedule(() -> {
|
||||
if (uplinkTime == client.getLastUplinkTime()) {
|
||||
asleep(client);
|
||||
}
|
||||
return null;
|
||||
}, timeout, TimeUnit.MILLISECONDS);
|
||||
client.setSleepTask(task);
|
||||
if (notifyOtherServers && partitionService.countTransportsByType(DataConstants.COAP_TRANSPORT_NAME) > 1) {
|
||||
transportService.notifyAboutUplink(getNewSyncSession(client), TransportProtos.UplinkNotificationMsg.newBuilder().setUplinkTs(uplinkTime).build(), TransportServiceCallback.EMPTY);
|
||||
}
|
||||
} finally {
|
||||
client.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private long getTimeout(TbCoapClientState client, PowerMode powerMode, PowerSavingConfiguration profileSettings) {
|
||||
long timeout;
|
||||
if (PowerMode.PSM.equals(powerMode)) {
|
||||
Long psmActivityTimer = client.getPsmActivityTimer();
|
||||
if (psmActivityTimer == null && profileSettings != null) {
|
||||
psmActivityTimer = profileSettings.getPsmActivityTimer();
|
||||
|
||||
}
|
||||
if (psmActivityTimer == null || psmActivityTimer == 0L) {
|
||||
psmActivityTimer = config.getPsmActivityTimer();
|
||||
}
|
||||
|
||||
timeout = psmActivityTimer;
|
||||
} else {
|
||||
Long pagingTransmissionWindow = client.getPagingTransmissionWindow();
|
||||
if (pagingTransmissionWindow == null && profileSettings != null) {
|
||||
pagingTransmissionWindow = profileSettings.getPagingTransmissionWindow();
|
||||
|
||||
}
|
||||
if (pagingTransmissionWindow == null || pagingTransmissionWindow == 0L) {
|
||||
pagingTransmissionWindow = config.getPagingTransmissionWindow();
|
||||
}
|
||||
timeout = pagingTransmissionWindow;
|
||||
}
|
||||
return timeout;
|
||||
}
|
||||
|
||||
private boolean registerFeatureObservation(TbCoapClientState state, String token, CoapExchange exchange, FeatureType featureType) {
|
||||
state.lock();
|
||||
try {
|
||||
boolean newObservation;
|
||||
if (FeatureType.ATTRIBUTES.equals(featureType)) {
|
||||
if (state.getAttrs() == null) {
|
||||
newObservation = true;
|
||||
state.setAttrs(new TbCoapObservationState(exchange, token));
|
||||
} else {
|
||||
newObservation = !state.getAttrs().getToken().equals(token);
|
||||
if (newObservation) {
|
||||
TbCoapObservationState old = state.getAttrs();
|
||||
state.setAttrs(new TbCoapObservationState(exchange, token));
|
||||
old.getExchange().respond(CoAP.ResponseCode.DELETED);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (state.getRpc() == null) {
|
||||
newObservation = true;
|
||||
state.setRpc(new TbCoapObservationState(exchange, token));
|
||||
} else {
|
||||
newObservation = !state.getRpc().getToken().equals(token);
|
||||
if (newObservation) {
|
||||
TbCoapObservationState old = state.getRpc();
|
||||
state.setRpc(new TbCoapObservationState(exchange, token));
|
||||
old.getExchange().respond(CoAP.ResponseCode.DELETED);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newObservation) {
|
||||
clientsByToken.put(token, state);
|
||||
if (state.getSession() == null) {
|
||||
TransportProtos.SessionInfoProto session = SessionInfoCreator.create(state.getCredentials(), transportContext, UUID.randomUUID());
|
||||
state.setSession(session);
|
||||
CoapSessionListener listener = new CoapSessionListener(state);
|
||||
state.setListener(listener);
|
||||
transportService.registerAsyncSession(session, state.getListener());
|
||||
transportService.process(session, getSessionEventMsg(TransportProtos.SessionEvent.OPEN), null);
|
||||
}
|
||||
if (FeatureType.ATTRIBUTES.equals(featureType)) {
|
||||
transportService.process(state.getSession(),
|
||||
TransportProtos.SubscribeToAttributeUpdatesMsg.getDefaultInstance(), new CoapNoOpCallback(exchange));
|
||||
transportService.process(state.getSession(),
|
||||
TransportProtos.GetAttributeRequestMsg.newBuilder().setOnlyShared(true).build(),
|
||||
new CoapNoOpCallback(exchange));
|
||||
} else {
|
||||
transportService.process(state.getSession(),
|
||||
TransportProtos.SubscribeToRPCMsg.getDefaultInstance(),
|
||||
new CoapOkCallback(exchange, CoAP.ResponseCode.VALID, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)
|
||||
);
|
||||
}
|
||||
}
|
||||
return newObservation;
|
||||
} finally {
|
||||
state.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deregisterAttributeObservation(TbCoapClientState state, String token, CoapExchange exchange) {
|
||||
state.lock();
|
||||
try {
|
||||
clientsByToken.remove(token);
|
||||
if (state.getSession() == null) {
|
||||
log.trace("[{}] Failed to delete attribute observation: {}. Session is not present.", state.getDeviceId(), token);
|
||||
return;
|
||||
}
|
||||
if (state.getAttrs() == null) {
|
||||
log.trace("[{}] Failed to delete attribute observation: {}. It is not registered.", state.getDeviceId(), token);
|
||||
return;
|
||||
}
|
||||
if (!state.getAttrs().getToken().equals(token)) {
|
||||
log.trace("[{}] Failed to delete attribute observation: {}. Token mismatch.", state.getDeviceId(), token);
|
||||
return;
|
||||
}
|
||||
cancelAttributeSubscription(state);
|
||||
} finally {
|
||||
state.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deregisterRpcObservation(TbCoapClientState state, String token, CoapExchange exchange) {
|
||||
state.lock();
|
||||
try {
|
||||
clientsByToken.remove(token);
|
||||
if (state.getSession() == null) {
|
||||
log.trace("[{}] Failed to delete rpc observation: {}. Session is not present.", state.getDeviceId(), token);
|
||||
return;
|
||||
}
|
||||
if (state.getRpc() == null) {
|
||||
log.trace("[{}] Failed to delete rpc observation: {}. It is not registered.", state.getDeviceId(), token);
|
||||
return;
|
||||
}
|
||||
if (!state.getRpc().getToken().equals(token)) {
|
||||
log.trace("[{}] Failed to delete rpc observation: {}. Token mismatch.", state.getDeviceId(), token);
|
||||
return;
|
||||
}
|
||||
cancelRpcSubscription(state);
|
||||
} finally {
|
||||
state.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TbCoapClientState getOrCreateClient(SessionMsgType type, ValidateDeviceCredentialsResponse deviceCredentials, DeviceProfile deviceProfile) throws AdaptorException {
|
||||
DeviceId deviceId = deviceCredentials.getDeviceInfo().getDeviceId();
|
||||
TbCoapClientState state = getClientState(deviceId);
|
||||
state.lock();
|
||||
try {
|
||||
if (state.getConfiguration() == null || state.getAdaptor() == null) {
|
||||
initStateAdaptor(deviceProfile, state);
|
||||
}
|
||||
if (state.getCredentials() == null) {
|
||||
state.init(deviceCredentials);
|
||||
}
|
||||
} finally {
|
||||
state.unlock();
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportProtos.SessionInfoProto getNewSyncSession(TbCoapClientState state) {
|
||||
return SessionInfoCreator.create(state.getCredentials(), transportContext, UUID.randomUUID());
|
||||
}
|
||||
|
||||
private TbCoapClientState getClientState(DeviceId deviceId) {
|
||||
return clients.computeIfAbsent(deviceId, TbCoapClientState::new);
|
||||
}
|
||||
|
||||
private static TransportProtos.SessionEventMsg getSessionEventMsg(TransportProtos.SessionEvent event) {
|
||||
return TransportProtos.SessionEventMsg.newBuilder()
|
||||
.setSessionType(TransportProtos.SessionType.ASYNC)
|
||||
.setEvent(event).build();
|
||||
}
|
||||
|
||||
private TransportConfigurationContainer getTransportConfigurationContainer(DeviceProfile deviceProfile) throws AdaptorException {
|
||||
DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration();
|
||||
if (transportConfiguration instanceof DefaultDeviceProfileTransportConfiguration) {
|
||||
return new TransportConfigurationContainer(true);
|
||||
} else if (transportConfiguration instanceof CoapDeviceProfileTransportConfiguration) {
|
||||
CoapDeviceProfileTransportConfiguration coapDeviceProfileTransportConfiguration =
|
||||
(CoapDeviceProfileTransportConfiguration) transportConfiguration;
|
||||
CoapDeviceTypeConfiguration coapDeviceTypeConfiguration =
|
||||
coapDeviceProfileTransportConfiguration.getCoapDeviceTypeConfiguration();
|
||||
if (coapDeviceTypeConfiguration instanceof DefaultCoapDeviceTypeConfiguration) {
|
||||
DefaultCoapDeviceTypeConfiguration defaultCoapDeviceTypeConfiguration =
|
||||
(DefaultCoapDeviceTypeConfiguration) coapDeviceTypeConfiguration;
|
||||
TransportPayloadTypeConfiguration transportPayloadTypeConfiguration =
|
||||
defaultCoapDeviceTypeConfiguration.getTransportPayloadTypeConfiguration();
|
||||
if (transportPayloadTypeConfiguration instanceof JsonTransportPayloadConfiguration) {
|
||||
return new TransportConfigurationContainer(true);
|
||||
} else {
|
||||
ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration =
|
||||
(ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration;
|
||||
String deviceTelemetryProtoSchema = protoTransportPayloadConfiguration.getDeviceTelemetryProtoSchema();
|
||||
String deviceAttributesProtoSchema = protoTransportPayloadConfiguration.getDeviceAttributesProtoSchema();
|
||||
String deviceRpcRequestProtoSchema = protoTransportPayloadConfiguration.getDeviceRpcRequestProtoSchema();
|
||||
String deviceRpcResponseProtoSchema = protoTransportPayloadConfiguration.getDeviceRpcResponseProtoSchema();
|
||||
return new TransportConfigurationContainer(false,
|
||||
protoTransportPayloadConfiguration.getTelemetryDynamicMessageDescriptor(deviceTelemetryProtoSchema),
|
||||
protoTransportPayloadConfiguration.getAttributesDynamicMessageDescriptor(deviceAttributesProtoSchema),
|
||||
protoTransportPayloadConfiguration.getRpcResponseDynamicMessageDescriptor(deviceRpcResponseProtoSchema),
|
||||
protoTransportPayloadConfiguration.getRpcRequestDynamicMessageBuilder(deviceRpcRequestProtoSchema)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new AdaptorException("Invalid CoapDeviceTypeConfiguration type: " + coapDeviceTypeConfiguration.getClass().getSimpleName() + "!");
|
||||
}
|
||||
} else {
|
||||
throw new AdaptorException("Invalid DeviceProfileTransportConfiguration type" + transportConfiguration.getClass().getSimpleName() + "!");
|
||||
}
|
||||
}
|
||||
|
||||
private void initStateAdaptor(DeviceProfile deviceProfile, TbCoapClientState state) throws AdaptorException {
|
||||
state.setConfiguration(getTransportConfigurationContainer(deviceProfile));
|
||||
state.setAdaptor(getCoapTransportAdaptor(state.getConfiguration().isJsonPayload()));
|
||||
state.setContentFormat(state.getAdaptor().getContentFormat());
|
||||
}
|
||||
|
||||
private CoapTransportAdaptor getCoapTransportAdaptor(boolean jsonPayloadType) {
|
||||
return jsonPayloadType ? transportContext.getJsonCoapAdaptor() : transportContext.getProtoCoapAdaptor();
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class CoapSessionListener implements SessionMsgListener {
|
||||
|
||||
private final TbCoapClientState state;
|
||||
|
||||
@Override
|
||||
public void onGetAttributesResponse(TransportProtos.GetAttributeResponseMsg msg) {
|
||||
TbCoapObservationState attrs = state.getAttrs();
|
||||
if (attrs != null) {
|
||||
try {
|
||||
Response response = state.getAdaptor().convertToPublish(msg);
|
||||
respond(attrs.getExchange(), response, state.getContentFormat());
|
||||
} catch (AdaptorException e) {
|
||||
log.trace("Failed to reply due to error", e);
|
||||
cancelObserveRelation(attrs);
|
||||
cancelAttributeSubscription(state);
|
||||
}
|
||||
} else {
|
||||
log.debug("[{}] Get Attrs exchange is empty", state.getDeviceId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttributeUpdate(UUID sessionId, TransportProtos.AttributeUpdateNotificationMsg msg) {
|
||||
if (!isDownlinkAllowed(state)) {
|
||||
log.trace("[{}] ignore downlink request cause client is sleeping.", state.getDeviceId());
|
||||
state.lock();
|
||||
try {
|
||||
state.addQueuedNotification(msg);
|
||||
} finally {
|
||||
state.unlock();
|
||||
}
|
||||
return;
|
||||
}
|
||||
log.trace("[{}] Received attributes update notification to device", sessionId);
|
||||
TbCoapObservationState attrs = state.getAttrs();
|
||||
if (attrs != null) {
|
||||
try {
|
||||
boolean conRequest = AbstractSyncSessionCallback.isConRequest(state.getAttrs());
|
||||
int requestId = getNextMsgId();
|
||||
Response response = state.getAdaptor().convertToPublish(msg);
|
||||
response.setConfirmable(conRequest);
|
||||
response.setMID(requestId);
|
||||
if (conRequest) {
|
||||
response.addMessageObserver(new TbCoapMessageObserver(requestId, id -> awake(state), id -> asleep(state)));
|
||||
}
|
||||
respond(attrs.getExchange(), response, state.getContentFormat());
|
||||
} catch (AdaptorException e) {
|
||||
log.trace("[{}] Failed to reply due to error", state.getDeviceId(), e);
|
||||
cancelObserveRelation(attrs);
|
||||
cancelAttributeSubscription(state);
|
||||
}
|
||||
} else {
|
||||
log.debug("[{}] Get Attrs exchange is empty", state.getDeviceId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeviceProfileUpdate(TransportProtos.SessionInfoProto newSessionInfo, DeviceProfile deviceProfile) {
|
||||
try {
|
||||
initStateAdaptor(deviceProfile, state);
|
||||
} catch (AdaptorException e) {
|
||||
log.warn("[{}] Failed to update device profile: ", deviceProfile.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional<DeviceProfile> deviceProfileOpt) {
|
||||
if (deviceProfileOpt.isPresent()) {
|
||||
try {
|
||||
initStateAdaptor(deviceProfileOpt.get(), state);
|
||||
} catch (AdaptorException e) {
|
||||
log.warn("[{}] Failed to update device: ", device.getId(), e);
|
||||
}
|
||||
}
|
||||
state.onDeviceUpdate(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeviceDeleted(DeviceId deviceId) {
|
||||
cancelRpcSubscription(state);
|
||||
cancelAttributeSubscription(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoteSessionCloseCommand(UUID sessionId, TransportProtos.SessionCloseNotificationProto sessionCloseNotification) {
|
||||
log.trace("[{}] Received the remote command to close the session: {}", sessionId, sessionCloseNotification.getMessage());
|
||||
cancelRpcSubscription(state);
|
||||
cancelAttributeSubscription(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToDeviceRpcRequest(UUID sessionId, TransportProtos.ToDeviceRpcRequestMsg msg) {
|
||||
log.trace("[{}] Received RPC command to device", sessionId);
|
||||
if (!isDownlinkAllowed(state)) {
|
||||
log.trace("[{}] ignore downlink request cause client is sleeping.", state.getDeviceId());
|
||||
return;
|
||||
}
|
||||
boolean sent = false;
|
||||
String error = null;
|
||||
boolean conRequest = AbstractSyncSessionCallback.isConRequest(state.getRpc());
|
||||
try {
|
||||
Response response = state.getAdaptor().convertToPublish(msg, state.getConfiguration().getRpcRequestDynamicMessageBuilder());
|
||||
response.setConfirmable(conRequest);
|
||||
int requestId = getNextMsgId();
|
||||
response.setMID(requestId);
|
||||
if (conRequest) {
|
||||
PowerMode powerMode = state.getPowerMode();
|
||||
PowerSavingConfiguration profileSettings = null;
|
||||
if (powerMode == null) {
|
||||
var clientProfile = getProfile(state.getProfileId());
|
||||
if (clientProfile.isPresent()) {
|
||||
profileSettings = clientProfile.get().getClientSettings();
|
||||
if (profileSettings != null) {
|
||||
powerMode = profileSettings.getPowerMode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transportContext.getRpcAwaitingAck().put(requestId, msg);
|
||||
transportContext.getScheduler().schedule(() -> {
|
||||
TransportProtos.ToDeviceRpcRequestMsg rpcRequestMsg = transportContext.getRpcAwaitingAck().remove(requestId);
|
||||
if (rpcRequestMsg != null) {
|
||||
transportService.process(state.getSession(), msg, RpcStatus.TIMEOUT, TransportServiceCallback.EMPTY);
|
||||
}
|
||||
}, Math.min(getTimeout(state, powerMode, profileSettings), msg.getExpirationTime() - System.currentTimeMillis()), TimeUnit.MILLISECONDS);
|
||||
|
||||
response.addMessageObserver(new TbCoapMessageObserver(requestId, id -> {
|
||||
TransportProtos.ToDeviceRpcRequestMsg rpcRequestMsg = transportContext.getRpcAwaitingAck().remove(id);
|
||||
if (rpcRequestMsg != null) {
|
||||
transportService.process(state.getSession(), rpcRequestMsg, RpcStatus.DELIVERED, true, TransportServiceCallback.EMPTY);
|
||||
}
|
||||
}, id -> {
|
||||
TransportProtos.ToDeviceRpcRequestMsg rpcRequestMsg = transportContext.getRpcAwaitingAck().remove(id);
|
||||
if (rpcRequestMsg != null) {
|
||||
transportService.process(state.getSession(), msg, RpcStatus.TIMEOUT, TransportServiceCallback.EMPTY);
|
||||
}
|
||||
}));
|
||||
}
|
||||
if (conRequest) {
|
||||
response.addMessageObserver(new TbCoapMessageObserver(requestId, id -> awake(state), id -> asleep(state)));
|
||||
}
|
||||
respond(state.getRpc().getExchange(), response, state.getContentFormat());
|
||||
sent = true;
|
||||
} catch (AdaptorException e) {
|
||||
log.trace("Failed to reply due to error", e);
|
||||
cancelObserveRelation(state.getRpc());
|
||||
cancelRpcSubscription(state);
|
||||
error = "Failed to convert device RPC command to CoAP msg";
|
||||
} catch (Exception e) {
|
||||
error = "Internal error: " + e.getMessage();
|
||||
} finally {
|
||||
if (StringUtils.isNotEmpty(error)) {
|
||||
transportService.process(state.getSession(),
|
||||
TransportProtos.ToDeviceRpcResponseMsg.newBuilder()
|
||||
.setRequestId(msg.getRequestId()).setError(error).build(), TransportServiceCallback.EMPTY);
|
||||
} else if (sent) {
|
||||
if (!conRequest) {
|
||||
transportService.process(state.getSession(), msg, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY);
|
||||
} else if (msg.getPersisted()) {
|
||||
transportService.process(state.getSession(), msg, RpcStatus.SENT, TransportServiceCallback.EMPTY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg msg) {
|
||||
log.trace("[{}] Received server rpc response in the wrong session.", state.getSession());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUplinkNotification(TransportProtos.UplinkNotificationMsg notificationMsg) {
|
||||
awake(state, false, notificationMsg.getUplinkTs());
|
||||
}
|
||||
|
||||
private void cancelObserveRelation(TbCoapObservationState attrs) {
|
||||
if (attrs.getObserveRelation() != null) {
|
||||
attrs.getObserveRelation().cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean asleep(TbCoapClientState client) {
|
||||
boolean changed = compareAndSetSleepFlag(client, true);
|
||||
if (changed) {
|
||||
log.debug("[{}] client is sleeping", client.getDeviceId());
|
||||
transportService.log(client.getSession(), "Info: Client is sleeping!");
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awake(TbCoapClientState client) {
|
||||
return awake(client, true, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
private boolean awake(TbCoapClientState client, boolean notifyOtherServers, long uplinkTs) {
|
||||
onUplink(client, notifyOtherServers, uplinkTs);
|
||||
boolean changed = compareAndSetSleepFlag(client, false);
|
||||
if (changed) {
|
||||
log.debug("[{}] client is awake", client.getDeviceId());
|
||||
transportService.log(client.getSession(), "Info: Client is awake!");
|
||||
sendMsgsAfterSleeping(client);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private void sendMsgsAfterSleeping(TbCoapClientState client) {
|
||||
if (client.getRpc() != null) {
|
||||
TransportProtos.TransportToDeviceActorMsg persistentRpcRequestMsg = TransportProtos.TransportToDeviceActorMsg
|
||||
.newBuilder()
|
||||
.setSessionInfo(client.getSession())
|
||||
.setSendPendingRPC(TransportProtos.SendPendingRPCMsg.newBuilder().build())
|
||||
.build();
|
||||
transportService.process(persistentRpcRequestMsg, TransportServiceCallback.EMPTY);
|
||||
}
|
||||
if (client.getAttrs() != null && client.getMissedAttributeUpdates() != null) {
|
||||
client.getListener().onAttributeUpdate(new UUID(client.getSession().getSessionIdMSB(), client.getSession().getSessionIdLSB()), client.getAndClearMissedUpdates());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean compareAndSetSleepFlag(TbCoapClientState client, boolean sleeping) {
|
||||
if (sleeping == client.isAsleep()) {
|
||||
log.trace("[{}] Client is already at sleeping: {}, ignoring event: {}", client.getDeviceId(), client.isAsleep(), sleeping);
|
||||
return false;
|
||||
}
|
||||
client.lock();
|
||||
try {
|
||||
if (sleeping == client.isAsleep()) {
|
||||
log.trace("[{}] Client is already at sleeping: {}, ignoring event: {}", client.getDeviceId(), client.isAsleep(), sleeping);
|
||||
return false;
|
||||
} else {
|
||||
PowerMode powerMode = getPowerMode(client);
|
||||
if (PowerMode.PSM.equals(powerMode) || PowerMode.E_DRX.equals(powerMode)) {
|
||||
log.trace("[{}] Switch sleeping from: {} to: {}", client.getDeviceId(), client.isAsleep(), sleeping);
|
||||
client.setAsleep(sleeping);
|
||||
// TODO: persist changes.
|
||||
// update(client);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
client.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDownlinkAllowed(TbCoapClientState client) {
|
||||
PowerMode powerMode = client.getPowerMode();
|
||||
PowerSavingConfiguration profileSettings = null;
|
||||
if (powerMode == null) {
|
||||
var clientProfile = getProfile(client.getProfileId());
|
||||
if (clientProfile.isPresent()) {
|
||||
profileSettings = clientProfile.get().getClientSettings();
|
||||
if (profileSettings != null) {
|
||||
powerMode = profileSettings.getPowerMode();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (powerMode == null || PowerMode.DRX.equals(powerMode)) {
|
||||
return true;
|
||||
}
|
||||
client.lock();
|
||||
long timeSinceLastUplink = System.currentTimeMillis() - client.getLastUplinkTime();
|
||||
try {
|
||||
if (PowerMode.PSM.equals(powerMode)) {
|
||||
Long psmActivityTimer = client.getPsmActivityTimer();
|
||||
if (psmActivityTimer == null && profileSettings != null) {
|
||||
psmActivityTimer = profileSettings.getPsmActivityTimer();
|
||||
|
||||
}
|
||||
if (psmActivityTimer == null || psmActivityTimer == 0L) {
|
||||
psmActivityTimer = config.getPsmActivityTimer();
|
||||
}
|
||||
return timeSinceLastUplink <= psmActivityTimer;
|
||||
} else {
|
||||
Long pagingTransmissionWindow = client.getPagingTransmissionWindow();
|
||||
if (pagingTransmissionWindow == null && profileSettings != null) {
|
||||
pagingTransmissionWindow = profileSettings.getPagingTransmissionWindow();
|
||||
|
||||
}
|
||||
if (pagingTransmissionWindow == null || pagingTransmissionWindow == 0L) {
|
||||
pagingTransmissionWindow = config.getPagingTransmissionWindow();
|
||||
}
|
||||
boolean allowed = timeSinceLastUplink <= pagingTransmissionWindow;
|
||||
if (!allowed) {
|
||||
return client.checkFirstDownlink();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
client.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private PowerMode getPowerMode(TbCoapClientState client) {
|
||||
PowerMode powerMode = client.getPowerMode();
|
||||
if (powerMode == null) {
|
||||
Optional<CoapDeviceProfileTransportConfiguration> deviceProfile = getProfile(client.getProfileId());
|
||||
if (deviceProfile.isPresent()) {
|
||||
powerMode = deviceProfile.get().getClientSettings().getPowerMode();
|
||||
} else {
|
||||
powerMode = PowerMode.PSM;
|
||||
}
|
||||
}
|
||||
return powerMode;
|
||||
}
|
||||
|
||||
public Optional<CoapDeviceProfileTransportConfiguration> getProfile(DeviceProfileId profileId) {
|
||||
DeviceProfile deviceProfile = profileCache.get(profileId);
|
||||
if (deviceProfile.getTransportType().equals(DeviceTransportType.COAP)) {
|
||||
return Optional.of((CoapDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration());
|
||||
} else if (deviceProfile.getTransportType().equals(DeviceTransportType.DEFAULT)) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
log.warn("[{}] Invalid device profile type: {}", profileId, deviceProfile.getTransportType());
|
||||
throw new IllegalArgumentException("Invalid device profile type: " + deviceProfile.getTransportType());
|
||||
}
|
||||
}
|
||||
|
||||
protected int getNextMsgId() {
|
||||
return ThreadLocalRandom.current().nextInt(NONE, MAX_MID + 1);
|
||||
}
|
||||
|
||||
private void cancelRpcSubscription(TbCoapClientState state) {
|
||||
if (state.getRpc() != null) {
|
||||
clientsByToken.remove(state.getRpc().getToken());
|
||||
CoapExchange exchange = state.getRpc().getExchange();
|
||||
state.setRpc(null);
|
||||
transportService.process(state.getSession(),
|
||||
TransportProtos.SubscribeToRPCMsg.newBuilder().setUnsubscribe(true).build(),
|
||||
new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
|
||||
if (state.getAttrs() == null) {
|
||||
closeAndCleanup(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelAttributeSubscription(TbCoapClientState state) {
|
||||
if (state.getAttrs() != null) {
|
||||
clientsByToken.remove(state.getAttrs().getToken());
|
||||
CoapExchange exchange = state.getAttrs().getExchange();
|
||||
state.setAttrs(null);
|
||||
transportService.process(state.getSession(),
|
||||
TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setUnsubscribe(true).build(),
|
||||
new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
|
||||
if (state.getRpc() == null) {
|
||||
closeAndCleanup(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void closeAndCleanup(TbCoapClientState state) {
|
||||
transportService.process(state.getSession(), getSessionEventMsg(TransportProtos.SessionEvent.CLOSED), null);
|
||||
transportService.deregisterSession(state.getSession());
|
||||
state.setSession(null);
|
||||
state.setConfiguration(null);
|
||||
state.setCredentials(null);
|
||||
state.setAdaptor(null);
|
||||
//TODO: add optimistic lock check that the client was already deleted and cleanup "clients" map.
|
||||
}
|
||||
|
||||
private void respond(CoapExchange exchange, Response response, int defContentFormat) {
|
||||
response.getOptions().setContentFormat(TbCoapContentFormatUtil.getContentFormat(exchange.getRequestOptions().getContentFormat(), defContentFormat));
|
||||
exchange.respond(response);
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.client;
|
||||
|
||||
import org.eclipse.californium.core.CoapClient;
|
||||
import org.eclipse.californium.core.CoapResponse;
|
||||
import org.eclipse.californium.core.Utils;
|
||||
import org.eclipse.californium.elements.DtlsEndpointContext;
|
||||
import org.eclipse.californium.elements.EndpointContext;
|
||||
import org.eclipse.californium.elements.exception.ConnectorException;
|
||||
import org.thingsboard.common.util.ThingsBoardThreadFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.Principal;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class NoSecClient {
|
||||
|
||||
private ExecutorService executor = Executors.newFixedThreadPool(1, ThingsBoardThreadFactory.forName(getClass().getSimpleName()));
|
||||
private CoapClient coapClient;
|
||||
|
||||
public NoSecClient(String host, int port, String accessToken, String clientKeys, String sharedKeys) throws URISyntaxException {
|
||||
URI uri = new URI(getFutureUrl(host, port, accessToken, clientKeys, sharedKeys));
|
||||
this.coapClient = new CoapClient(uri);
|
||||
}
|
||||
|
||||
public void test() {
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
while (!Thread.interrupted()) {
|
||||
CoapResponse response = null;
|
||||
try {
|
||||
response = coapClient.get();
|
||||
} catch (ConnectorException | IOException e) {
|
||||
System.err.println("Error occurred while sending request: " + e);
|
||||
System.exit(-1);
|
||||
}
|
||||
if (response != null) {
|
||||
|
||||
System.out.println(response.getCode() + " - " + response.getCode().name());
|
||||
System.out.println(response.getOptions());
|
||||
System.out.println(response.getResponseText());
|
||||
System.out.println();
|
||||
System.out.println("ADVANCED:");
|
||||
EndpointContext context = response.advanced().getSourceContext();
|
||||
Principal identity = context.getPeerIdentity();
|
||||
if (identity != null) {
|
||||
System.out.println(context.getPeerIdentity());
|
||||
} else {
|
||||
System.out.println("anonymous");
|
||||
}
|
||||
System.out.println(context.get(DtlsEndpointContext.KEY_CIPHER));
|
||||
System.out.println(Utils.prettyPrint(response));
|
||||
} else {
|
||||
System.out.println("No response received.");
|
||||
}
|
||||
Thread.sleep(5000);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.out.println("Error occurred while sending COAP requests.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String getFutureUrl(String host, Integer port, String accessToken, String clientKeys, String sharedKeys) {
|
||||
return "coap://" + host + ":" + port + "/api/v1/" + accessToken + "/attributes?clientKeys=" + clientKeys + "&sharedKeys=" + sharedKeys;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws URISyntaxException {
|
||||
System.out.println("Usage: java -cp ... org.thingsboard.server.transport.coap.client.NoSecClient " +
|
||||
"host port accessToken clientKeys sharedKeys");
|
||||
|
||||
String host = args[0];
|
||||
int port = Integer.parseInt(args[1]);
|
||||
String accessToken = args[2];
|
||||
String clientKeys = args[3];
|
||||
String sharedKeys = args[4];
|
||||
|
||||
NoSecClient client = new NoSecClient(host, port, accessToken, clientKeys, sharedKeys);
|
||||
client.test();
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.client;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.CoapClient;
|
||||
import org.eclipse.californium.core.CoapHandler;
|
||||
import org.eclipse.californium.core.CoapObserveRelation;
|
||||
import org.eclipse.californium.core.CoapResponse;
|
||||
import org.eclipse.californium.core.coap.CoAP;
|
||||
import org.eclipse.californium.core.coap.Request;
|
||||
import org.thingsboard.common.util.ThingsBoardThreadFactory;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@Slf4j
|
||||
public class NoSecObserveClient {
|
||||
|
||||
private static final long INFINIT_EXCHANGE_LIFETIME = 0L;
|
||||
|
||||
private CoapClient coapClient;
|
||||
private CoapObserveRelation observeRelation;
|
||||
private ExecutorService executor = Executors.newFixedThreadPool(1, ThingsBoardThreadFactory.forName(getClass().getSimpleName()));
|
||||
private CountDownLatch latch;
|
||||
|
||||
public NoSecObserveClient(String host, int port, String accessToken) throws URISyntaxException {
|
||||
URI uri = new URI(getFutureUrl(host, port, accessToken));
|
||||
this.coapClient = new CoapClient(uri);
|
||||
coapClient.setTimeout(INFINIT_EXCHANGE_LIFETIME);
|
||||
this.latch = new CountDownLatch(5);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
Request request = Request.newGet();
|
||||
request.setObserve();
|
||||
observeRelation = coapClient.observe(request, new CoapHandler() {
|
||||
@Override
|
||||
public void onLoad(CoapResponse response) {
|
||||
String responseText = response.getResponseText();
|
||||
CoAP.ResponseCode code = response.getCode();
|
||||
Integer observe = response.getOptions().getObserve();
|
||||
log.info("CoAP Response received! " +
|
||||
"responseText: {}, " +
|
||||
"code: {}, " +
|
||||
"observe seq number: {}",
|
||||
responseText,
|
||||
code,
|
||||
observe);
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError() {
|
||||
log.error("Ack error!");
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.error("Error occurred while sending COAP requests: ");
|
||||
}
|
||||
});
|
||||
try {
|
||||
latch.await();
|
||||
observeRelation.proactiveCancel();
|
||||
} catch (InterruptedException e) {
|
||||
log.error("Error occurred: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getFutureUrl(String host, Integer port, String accessToken) {
|
||||
return "coap://" + host + ":" + port + "/api/v1/" + accessToken + "/attributes";
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws URISyntaxException {
|
||||
log.info("Usage: java -cp ... org.thingsboard.server.transport.coap.client.NoSecObserveClient " +
|
||||
"host port accessToken");
|
||||
|
||||
String host = args[0];
|
||||
int port = Integer.parseInt(args[1]);
|
||||
String accessToken = args[2];
|
||||
|
||||
final NoSecObserveClient client = new NoSecObserveClient(host, port, accessToken);
|
||||
client.start();
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.client;
|
||||
|
||||
import org.eclipse.californium.core.CoapClient;
|
||||
import org.eclipse.californium.core.CoapResponse;
|
||||
import org.eclipse.californium.core.Utils;
|
||||
import org.eclipse.californium.core.network.CoapEndpoint;
|
||||
import org.eclipse.californium.elements.DtlsEndpointContext;
|
||||
import org.eclipse.californium.elements.EndpointContext;
|
||||
import org.eclipse.californium.elements.config.Configuration;
|
||||
import org.eclipse.californium.elements.exception.ConnectorException;
|
||||
import org.eclipse.californium.elements.util.SslContextUtil;
|
||||
import org.eclipse.californium.scandium.DTLSConnector;
|
||||
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
|
||||
import org.eclipse.californium.scandium.dtls.CertificateType;
|
||||
import org.eclipse.californium.scandium.dtls.x509.SingleCertificateProvider;
|
||||
import org.eclipse.californium.scandium.dtls.x509.StaticNewAdvancedCertificateVerifier;
|
||||
import org.thingsboard.common.util.ThingsBoardThreadFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.Principal;
|
||||
import java.security.cert.Certificate;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class SecureClientNoAuth {
|
||||
|
||||
private final DTLSConnector dtlsConnector;
|
||||
private ExecutorService executor = Executors.newFixedThreadPool(1, ThingsBoardThreadFactory.forName(getClass().getSimpleName()));
|
||||
private CoapClient coapClient;
|
||||
|
||||
public SecureClientNoAuth(DTLSConnector dtlsConnector, String host, int port, String accessToken, String clientKeys, String sharedKeys) throws URISyntaxException {
|
||||
this.dtlsConnector = dtlsConnector;
|
||||
this.coapClient = getCoapClient(host, port, accessToken, clientKeys, sharedKeys);
|
||||
}
|
||||
|
||||
public void test() {
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
while (!Thread.interrupted()) {
|
||||
CoapResponse response = null;
|
||||
try {
|
||||
response = coapClient.get();
|
||||
} catch (ConnectorException | IOException e) {
|
||||
System.err.println("Error occurred while sending request: " + e);
|
||||
System.exit(-1);
|
||||
}
|
||||
if (response != null) {
|
||||
|
||||
System.out.println(response.getCode() + " - " + response.getCode().name());
|
||||
System.out.println(response.getOptions());
|
||||
System.out.println(response.getResponseText());
|
||||
System.out.println();
|
||||
System.out.println("ADVANCED:");
|
||||
EndpointContext context = response.advanced().getSourceContext();
|
||||
Principal identity = context.getPeerIdentity();
|
||||
if (identity != null) {
|
||||
System.out.println(context.getPeerIdentity());
|
||||
} else {
|
||||
System.out.println("anonymous");
|
||||
}
|
||||
System.out.println(context.get(DtlsEndpointContext.KEY_CIPHER));
|
||||
System.out.println(Utils.prettyPrint(response));
|
||||
} else {
|
||||
System.out.println("No response received.");
|
||||
}
|
||||
Thread.sleep(5000);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.out.println("Error occurred while sending COAP requests.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private CoapClient getCoapClient(String host, Integer port, String accessToken, String clientKeys, String sharedKeys) throws URISyntaxException {
|
||||
URI uri = new URI(getFutureUrl(host, port, accessToken, clientKeys, sharedKeys));
|
||||
CoapClient client = new CoapClient(uri);
|
||||
CoapEndpoint.Builder builder = new CoapEndpoint.Builder();
|
||||
builder.setConnector(dtlsConnector);
|
||||
|
||||
client.setEndpoint(builder.build());
|
||||
return client;
|
||||
}
|
||||
|
||||
private String getFutureUrl(String host, Integer port, String accessToken, String clientKeys, String sharedKeys) {
|
||||
return "coaps://" + host + ":" + port + "/api/v1/" + accessToken + "/attributes?clientKeys=" + clientKeys + "&sharedKeys=" + sharedKeys;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws URISyntaxException {
|
||||
System.out.println("Usage: java -cp ... org.thingsboard.server.transport.coap.client.SecureClientNoAuth " +
|
||||
"host port accessToken keyStoreUriPath keyStoreAlias trustedAliasPattern clientKeys sharedKeys");
|
||||
|
||||
String host = args[0];
|
||||
int port = Integer.parseInt(args[1]);
|
||||
String accessToken = args[2];
|
||||
String clientKeys = args[7];
|
||||
String sharedKeys = args[8];
|
||||
|
||||
String keyStoreUriPath = args[3];
|
||||
String keyStoreAlias = args[4];
|
||||
String trustedAliasPattern = args[5];
|
||||
String keyStorePassword = args[6];
|
||||
|
||||
|
||||
DtlsConnectorConfig.Builder builder = new DtlsConnectorConfig.Builder(new Configuration());
|
||||
setupCredentials(builder, keyStoreUriPath, keyStoreAlias, trustedAliasPattern, keyStorePassword);
|
||||
DTLSConnector dtlsConnector = new DTLSConnector(builder.build());
|
||||
SecureClientNoAuth client = new SecureClientNoAuth(dtlsConnector, host, port, accessToken, clientKeys, sharedKeys);
|
||||
client.test();
|
||||
}
|
||||
|
||||
private static void setupCredentials(DtlsConnectorConfig.Builder config, String keyStoreUriPath, String keyStoreAlias, String trustedAliasPattern, String keyStorePassword) {
|
||||
StaticNewAdvancedCertificateVerifier.Builder trustBuilder = StaticNewAdvancedCertificateVerifier.builder();
|
||||
try {
|
||||
SslContextUtil.Credentials serverCredentials = SslContextUtil.loadCredentials(
|
||||
keyStoreUriPath, keyStoreAlias, keyStorePassword.toCharArray(), keyStorePassword.toCharArray());
|
||||
Certificate[] trustedCertificates = SslContextUtil.loadTrustedCertificates(
|
||||
keyStoreUriPath, trustedAliasPattern, keyStorePassword.toCharArray());
|
||||
trustBuilder.setTrustedCertificates(trustedCertificates);
|
||||
config.setAdvancedCertificateVerifier(trustBuilder.build());
|
||||
config.setCertificateIdentityProvider(new SingleCertificateProvider(serverCredentials.getPrivateKey(), serverCredentials.getCertificateChain(), Collections.singletonList(CertificateType.X_509)));
|
||||
} catch (GeneralSecurityException e) {
|
||||
System.err.println("certificates are invalid!");
|
||||
throw new IllegalArgumentException(e.getMessage());
|
||||
} catch (IOException e) {
|
||||
System.err.println("certificates are missing!");
|
||||
throw new IllegalArgumentException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.client;
|
||||
|
||||
import org.eclipse.californium.core.CoapClient;
|
||||
import org.eclipse.californium.core.CoapResponse;
|
||||
import org.eclipse.californium.core.Utils;
|
||||
import org.eclipse.californium.core.network.CoapEndpoint;
|
||||
import org.eclipse.californium.elements.DtlsEndpointContext;
|
||||
import org.eclipse.californium.elements.EndpointContext;
|
||||
import org.eclipse.californium.elements.config.Configuration;
|
||||
import org.eclipse.californium.elements.exception.ConnectorException;
|
||||
import org.eclipse.californium.elements.util.SslContextUtil;
|
||||
import org.eclipse.californium.scandium.DTLSConnector;
|
||||
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
|
||||
import org.eclipse.californium.scandium.dtls.CertificateType;
|
||||
import org.eclipse.californium.scandium.dtls.x509.SingleCertificateProvider;
|
||||
import org.eclipse.californium.scandium.dtls.x509.StaticNewAdvancedCertificateVerifier;
|
||||
import org.thingsboard.common.util.ThingsBoardThreadFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.Principal;
|
||||
import java.security.cert.Certificate;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class SecureClientX509 {
|
||||
|
||||
private final DTLSConnector dtlsConnector;
|
||||
private ExecutorService executor = Executors.newFixedThreadPool(1, ThingsBoardThreadFactory.forName(getClass().getSimpleName()));
|
||||
private CoapClient coapClient;
|
||||
|
||||
public SecureClientX509(DTLSConnector dtlsConnector, String host, int port, String clientKeys, String sharedKeys) throws URISyntaxException {
|
||||
this.dtlsConnector = dtlsConnector;
|
||||
this.coapClient = getCoapClient(host, port, clientKeys, sharedKeys);
|
||||
}
|
||||
|
||||
public void test() {
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
while (!Thread.interrupted()) {
|
||||
CoapResponse response = null;
|
||||
try {
|
||||
response = coapClient.get();
|
||||
} catch (ConnectorException | IOException e) {
|
||||
System.err.println("Error occurred while sending request: " + e);
|
||||
System.exit(-1);
|
||||
}
|
||||
if (response != null) {
|
||||
|
||||
System.out.println(response.getCode() + " - " + response.getCode().name());
|
||||
System.out.println(response.getOptions());
|
||||
System.out.println(response.getResponseText());
|
||||
System.out.println();
|
||||
System.out.println("ADVANCED:");
|
||||
EndpointContext context = response.advanced().getSourceContext();
|
||||
Principal identity = context.getPeerIdentity();
|
||||
if (identity != null) {
|
||||
System.out.println(context.getPeerIdentity());
|
||||
} else {
|
||||
System.out.println("anonymous");
|
||||
}
|
||||
System.out.println(context.get(DtlsEndpointContext.KEY_CIPHER));
|
||||
System.out.println(Utils.prettyPrint(response));
|
||||
} else {
|
||||
System.out.println("No response received.");
|
||||
}
|
||||
Thread.sleep(5000);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.out.println("Error occurred while sending COAP requests.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private CoapClient getCoapClient(String host, Integer port, String clientKeys, String sharedKeys) throws URISyntaxException {
|
||||
URI uri = new URI(getFutureUrl(host, port, clientKeys, sharedKeys));
|
||||
CoapClient client = new CoapClient(uri);
|
||||
CoapEndpoint.Builder builder = new CoapEndpoint.Builder();
|
||||
builder.setConnector(dtlsConnector);
|
||||
|
||||
client.setEndpoint(builder.build());
|
||||
return client;
|
||||
}
|
||||
|
||||
private String getFutureUrl(String host, Integer port, String clientKeys, String sharedKeys) {
|
||||
return "coaps://" + host + ":" + port + "/api/v1/attributes?clientKeys=" + clientKeys + "&sharedKeys=" + sharedKeys;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws URISyntaxException {
|
||||
System.out.println("Usage: java -cp ... org.thingsboard.server.transport.coap.client.SecureClientX509 " +
|
||||
"host port keyStoreUriPath keyStoreAlias trustedAliasPattern clientKeys sharedKeys");
|
||||
|
||||
String host = args[0];
|
||||
int port = Integer.parseInt(args[1]);
|
||||
String clientKeys = args[6];
|
||||
String sharedKeys = args[7];
|
||||
|
||||
String keyStoreUriPath = args[2];
|
||||
String keyStoreAlias = args[3];
|
||||
String trustedAliasPattern = args[4];
|
||||
String keyStorePassword = args[5];
|
||||
|
||||
|
||||
DtlsConnectorConfig.Builder builder = new DtlsConnectorConfig.Builder(new Configuration());
|
||||
setupCredentials(builder, keyStoreUriPath, keyStoreAlias, trustedAliasPattern, keyStorePassword);
|
||||
DTLSConnector dtlsConnector = new DTLSConnector(builder.build());
|
||||
SecureClientX509 client = new SecureClientX509(dtlsConnector, host, port, clientKeys, sharedKeys);
|
||||
client.test();
|
||||
}
|
||||
|
||||
private static void setupCredentials(DtlsConnectorConfig.Builder config, String keyStoreUriPath, String keyStoreAlias, String trustedAliasPattern, String keyStorePassword) {
|
||||
StaticNewAdvancedCertificateVerifier.Builder trustBuilder = StaticNewAdvancedCertificateVerifier.builder();
|
||||
try {
|
||||
SslContextUtil.Credentials serverCredentials = SslContextUtil.loadCredentials(
|
||||
keyStoreUriPath, keyStoreAlias, keyStorePassword.toCharArray(), keyStorePassword.toCharArray());
|
||||
Certificate[] trustedCertificates = SslContextUtil.loadTrustedCertificates(
|
||||
keyStoreUriPath, trustedAliasPattern, keyStorePassword.toCharArray());
|
||||
trustBuilder.setTrustedCertificates(trustedCertificates);
|
||||
config.setAdvancedCertificateVerifier(trustBuilder.build());
|
||||
config.setCertificateIdentityProvider(new SingleCertificateProvider(serverCredentials.getPrivateKey(), serverCredentials.getCertificateChain(), Collections.singletonList(CertificateType.X_509)));
|
||||
} catch (GeneralSecurityException e) {
|
||||
System.err.println("certificates are invalid!");
|
||||
throw new IllegalArgumentException(e.getMessage());
|
||||
} catch (IOException e) {
|
||||
System.err.println("certificates are missing!");
|
||||
throw new IllegalArgumentException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.client;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.thingsboard.server.common.data.Device;
|
||||
import org.thingsboard.server.common.data.DeviceTransportType;
|
||||
import org.thingsboard.server.common.data.device.data.CoapDeviceTransportConfiguration;
|
||||
import org.thingsboard.server.common.data.device.data.PowerMode;
|
||||
import org.thingsboard.server.common.data.id.DeviceId;
|
||||
import org.thingsboard.server.common.data.id.DeviceProfileId;
|
||||
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||
import org.thingsboard.server.transport.coap.TransportConfigurationContainer;
|
||||
import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@Data
|
||||
public class TbCoapClientState {
|
||||
|
||||
private final DeviceId deviceId;
|
||||
private final Lock lock;
|
||||
|
||||
private volatile TransportConfigurationContainer configuration;
|
||||
private volatile CoapTransportAdaptor adaptor;
|
||||
private volatile ValidateDeviceCredentialsResponse credentials;
|
||||
private volatile TransportProtos.SessionInfoProto session;
|
||||
private volatile DefaultCoapClientContext.CoapSessionListener listener;
|
||||
private volatile TbCoapObservationState attrs;
|
||||
private volatile TbCoapObservationState rpc;
|
||||
private volatile int contentFormat;
|
||||
|
||||
private TransportProtos.AttributeUpdateNotificationMsg missedAttributeUpdates;
|
||||
|
||||
private DeviceProfileId profileId;
|
||||
|
||||
@Getter
|
||||
private PowerMode powerMode;
|
||||
@Getter
|
||||
private Long psmActivityTimer;
|
||||
@Getter
|
||||
private Long edrxCycle;
|
||||
@Getter
|
||||
private Long pagingTransmissionWindow;
|
||||
@Getter
|
||||
@Setter
|
||||
private boolean asleep;
|
||||
@Getter
|
||||
private long lastUplinkTime;
|
||||
@Getter
|
||||
@Setter
|
||||
private Future<Void> sleepTask;
|
||||
|
||||
private boolean firstEdrxDownlink = true;
|
||||
|
||||
public TbCoapClientState(DeviceId deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
this.lock = new ReentrantLock();
|
||||
}
|
||||
|
||||
public void init(ValidateDeviceCredentialsResponse credentials) {
|
||||
this.credentials = credentials;
|
||||
this.profileId = credentials.getDeviceInfo().getDeviceProfileId();
|
||||
this.powerMode = credentials.getDeviceInfo().getPowerMode();
|
||||
this.edrxCycle = credentials.getDeviceInfo().getEdrxCycle();
|
||||
this.psmActivityTimer = credentials.getDeviceInfo().getPsmActivityTimer();
|
||||
this.pagingTransmissionWindow = credentials.getDeviceInfo().getPagingTransmissionWindow();
|
||||
}
|
||||
|
||||
public void lock() {
|
||||
lock.lock();
|
||||
}
|
||||
|
||||
public void unlock() {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
public long updateLastUplinkTime(long ts) {
|
||||
if (ts > lastUplinkTime) {
|
||||
this.lastUplinkTime = ts;
|
||||
this.firstEdrxDownlink = true;
|
||||
}
|
||||
return lastUplinkTime;
|
||||
}
|
||||
|
||||
public boolean checkFirstDownlink() {
|
||||
boolean result = firstEdrxDownlink;
|
||||
firstEdrxDownlink = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
public void onDeviceUpdate(Device device) {
|
||||
this.profileId = device.getDeviceProfileId();
|
||||
var data = device.getDeviceData();
|
||||
if (data.getTransportConfiguration() != null && data.getTransportConfiguration().getType().equals(DeviceTransportType.COAP)) {
|
||||
CoapDeviceTransportConfiguration configuration = (CoapDeviceTransportConfiguration) data.getTransportConfiguration();
|
||||
this.powerMode = configuration.getPowerMode();
|
||||
this.edrxCycle = configuration.getEdrxCycle();
|
||||
this.psmActivityTimer = configuration.getPsmActivityTimer();
|
||||
this.pagingTransmissionWindow = configuration.getPagingTransmissionWindow();
|
||||
}
|
||||
}
|
||||
|
||||
public void addQueuedNotification(TransportProtos.AttributeUpdateNotificationMsg msg) {
|
||||
if (missedAttributeUpdates == null) {
|
||||
missedAttributeUpdates = msg;
|
||||
} else {
|
||||
Map<String, TransportProtos.TsKvProto> updatedAttrs = new HashMap<>(missedAttributeUpdates.getSharedUpdatedCount() + msg.getSharedUpdatedCount());
|
||||
Set<String> deletedKeys = new HashSet<>(missedAttributeUpdates.getSharedDeletedCount() + msg.getSharedDeletedCount());
|
||||
for (TransportProtos.TsKvProto oldUpdatedAttrs : missedAttributeUpdates.getSharedUpdatedList()) {
|
||||
updatedAttrs.put(oldUpdatedAttrs.getKv().getKey(), oldUpdatedAttrs);
|
||||
}
|
||||
deletedKeys.addAll(msg.getSharedDeletedList());
|
||||
for (TransportProtos.TsKvProto newUpdatedAttrs : msg.getSharedUpdatedList()) {
|
||||
updatedAttrs.put(newUpdatedAttrs.getKv().getKey(), newUpdatedAttrs);
|
||||
}
|
||||
deletedKeys.addAll(msg.getSharedDeletedList());
|
||||
for (String deletedKey : msg.getSharedDeletedList()) {
|
||||
updatedAttrs.remove(deletedKey);
|
||||
}
|
||||
missedAttributeUpdates = TransportProtos.AttributeUpdateNotificationMsg.newBuilder().addAllSharedUpdated(updatedAttrs.values()).addAllSharedDeleted(deletedKeys).build();
|
||||
}
|
||||
}
|
||||
|
||||
public TransportProtos.AttributeUpdateNotificationMsg getAndClearMissedUpdates() {
|
||||
var result = this.missedAttributeUpdates;
|
||||
this.missedAttributeUpdates = null;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.client;
|
||||
|
||||
import org.eclipse.californium.core.coap.MediaTypeRegistry;
|
||||
|
||||
public class TbCoapContentFormatUtil {
|
||||
|
||||
public static int getContentFormat(int requestFormat, int adaptorFormat) {
|
||||
if (isStrict(adaptorFormat)) {
|
||||
return adaptorFormat;
|
||||
} else {
|
||||
return requestFormat != MediaTypeRegistry.UNDEFINED ? requestFormat : adaptorFormat;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isStrict(int contentFormat) {
|
||||
return contentFormat == MediaTypeRegistry.APPLICATION_OCTET_STREAM;
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.client;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.eclipse.californium.core.observe.ObserveRelation;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@Data
|
||||
@RequiredArgsConstructor
|
||||
public class TbCoapObservationState {
|
||||
|
||||
private final CoapExchange exchange;
|
||||
private final String token;
|
||||
private final AtomicInteger observeCounter = new AtomicInteger(0);
|
||||
private volatile ObserveRelation observeRelation;
|
||||
|
||||
}
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.efento;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.coap.CoAP;
|
||||
import org.eclipse.californium.core.coap.Request;
|
||||
import org.eclipse.californium.core.coap.Response;
|
||||
import org.eclipse.californium.core.network.Exchange;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
import org.eclipse.californium.core.server.resources.Resource;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.thingsboard.server.common.data.DeviceProfile;
|
||||
import org.thingsboard.server.common.data.DeviceTransportType;
|
||||
import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration;
|
||||
import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration;
|
||||
import org.thingsboard.server.common.data.device.profile.EfentoCoapDeviceTypeConfiguration;
|
||||
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
|
||||
import org.thingsboard.server.common.transport.auth.SessionInfoCreator;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||
import org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos;
|
||||
import org.thingsboard.server.gen.transport.coap.MeasurementsProtos;
|
||||
import org.thingsboard.server.transport.coap.AbstractCoapTransportResource;
|
||||
import org.thingsboard.server.transport.coap.CoapTransportContext;
|
||||
import org.thingsboard.server.transport.coap.callback.CoapDeviceAuthCallback;
|
||||
import org.thingsboard.server.transport.coap.callback.CoapEfentoCallback;
|
||||
import org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.thingsboard.server.transport.coap.CoapTransportService.CONFIGURATION;
|
||||
import static org.thingsboard.server.transport.coap.CoapTransportService.CURRENT_TIMESTAMP;
|
||||
import static org.thingsboard.server.transport.coap.CoapTransportService.DEVICE_INFO;
|
||||
import static org.thingsboard.server.transport.coap.CoapTransportService.MEASUREMENTS;
|
||||
|
||||
@Slf4j
|
||||
public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
|
||||
|
||||
private static final int CHILD_RESOURCE_POSITION = 2;
|
||||
|
||||
public CoapEfentoTransportResource(CoapTransportContext context, String name) {
|
||||
super(context, name);
|
||||
this.setObservable(true); // enable observing
|
||||
this.setObserveType(CoAP.Type.CON); // configure the notification type to CONs
|
||||
// this.getAttributes().setObservable(); // mark observable in the Link-Format
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processHandleGet(CoapExchange exchange) {
|
||||
Exchange advanced = exchange.advanced();
|
||||
Request request = advanced.getRequest();
|
||||
List<String> uriPath = request.getOptions().getUriPath();
|
||||
boolean validPath = uriPath.size() == CHILD_RESOURCE_POSITION && uriPath.get(1).equals(CURRENT_TIMESTAMP);
|
||||
if (!validPath) {
|
||||
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
|
||||
} else {
|
||||
int dateInSec = (int) (System.currentTimeMillis() / 1000);
|
||||
byte[] bytes = ByteBuffer.allocate(4).putInt(dateInSec).array();
|
||||
exchange.respond(CoAP.ResponseCode.CONTENT, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processHandlePost(CoapExchange exchange) {
|
||||
Exchange advanced = exchange.advanced();
|
||||
Request request = advanced.getRequest();
|
||||
List<String> uriPath = request.getOptions().getUriPath();
|
||||
if (uriPath.size() != CHILD_RESOURCE_POSITION) {
|
||||
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
|
||||
return;
|
||||
}
|
||||
String requestType = uriPath.get(1);
|
||||
switch (requestType) {
|
||||
case MEASUREMENTS:
|
||||
processMeasurementsRequest(exchange, request);
|
||||
break;
|
||||
case DEVICE_INFO:
|
||||
case CONFIGURATION:
|
||||
//We respond only to confirmed requests in order to reduce battery consumption for Efento devices.
|
||||
if (exchange.advanced().getRequest().isConfirmable()) {
|
||||
exchange.respond(new Response(CoAP.ResponseCode.CREATED));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void processMeasurementsRequest(CoapExchange exchange, Request request) {
|
||||
byte[] bytes = request.getPayload();
|
||||
try {
|
||||
MeasurementsProtos.ProtoMeasurements protoMeasurements = MeasurementsProtos.ProtoMeasurements.parseFrom(bytes);
|
||||
log.trace("Successfully parsed Efento ProtoMeasurements: [{}]", protoMeasurements.getCloudToken());
|
||||
String token = protoMeasurements.getCloudToken();
|
||||
transportService.process(DeviceTransportType.COAP, TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(token).build(),
|
||||
new CoapDeviceAuthCallback(exchange, (msg, deviceProfile) -> {
|
||||
TransportProtos.SessionInfoProto sessionInfo = SessionInfoCreator.create(msg, transportContext, UUID.randomUUID());
|
||||
UUID sessionId = new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB());
|
||||
try {
|
||||
validateEfentoTransportConfiguration(deviceProfile);
|
||||
List<EfentoMeasurements> efentoMeasurements = getEfentoMeasurements(protoMeasurements, sessionId);
|
||||
transportService.process(sessionInfo,
|
||||
transportContext.getEfentoCoapAdaptor().convertToPostTelemetry(sessionId, efentoMeasurements),
|
||||
new CoapEfentoCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
|
||||
reportSubscriptionInfo(sessionInfo, false, false);
|
||||
} catch (AdaptorException e) {
|
||||
log.error("[{}] Failed to decode Efento ProtoMeasurements: ", sessionId, e);
|
||||
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
|
||||
}
|
||||
}));
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to decode Efento ProtoMeasurements: ", e);
|
||||
exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource getChild(String name) {
|
||||
return this;
|
||||
}
|
||||
|
||||
private void validateEfentoTransportConfiguration(DeviceProfile deviceProfile) throws AdaptorException {
|
||||
DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration();
|
||||
if (transportConfiguration instanceof CoapDeviceProfileTransportConfiguration) {
|
||||
CoapDeviceProfileTransportConfiguration coapDeviceProfileTransportConfiguration =
|
||||
(CoapDeviceProfileTransportConfiguration) transportConfiguration;
|
||||
if (!(coapDeviceProfileTransportConfiguration.getCoapDeviceTypeConfiguration() instanceof EfentoCoapDeviceTypeConfiguration)) {
|
||||
throw new AdaptorException("Invalid CoapDeviceTypeConfiguration type: " + coapDeviceProfileTransportConfiguration.getCoapDeviceTypeConfiguration().getClass().getSimpleName() + "!");
|
||||
}
|
||||
} else {
|
||||
throw new AdaptorException("Invalid DeviceProfileTransportConfiguration type" + transportConfiguration.getClass().getSimpleName() + "!");
|
||||
}
|
||||
}
|
||||
|
||||
private List<EfentoMeasurements> getEfentoMeasurements(MeasurementsProtos.ProtoMeasurements protoMeasurements, UUID sessionId) {
|
||||
String serialNumber = CoapEfentoUtils.convertByteArrayToString(protoMeasurements.getSerialNum().toByteArray());
|
||||
boolean batteryStatus = protoMeasurements.getBatteryStatus();
|
||||
int measurementPeriodBase = protoMeasurements.getMeasurementPeriodBase();
|
||||
int measurementPeriodFactor = protoMeasurements.getMeasurementPeriodFactor();
|
||||
int signal = protoMeasurements.getSignal();
|
||||
List<MeasurementsProtos.ProtoChannel> channelsList = protoMeasurements.getChannelsList();
|
||||
Map<Long, JsonObject> valuesMap = new TreeMap<>();
|
||||
if (!CollectionUtils.isEmpty(channelsList)) {
|
||||
int channel = 0;
|
||||
JsonObject values;
|
||||
for (MeasurementsProtos.ProtoChannel protoChannel : channelsList) {
|
||||
channel++;
|
||||
boolean isBinarySensor = false;
|
||||
MeasurementTypeProtos.MeasurementType measurementType = protoChannel.getType();
|
||||
String measurementTypeName = measurementType.name();
|
||||
if (measurementType.equals(MeasurementTypeProtos.MeasurementType.OK_ALARM)
|
||||
|| measurementType.equals(MeasurementTypeProtos.MeasurementType.FLOODING)) {
|
||||
isBinarySensor = true;
|
||||
}
|
||||
if (measurementPeriodFactor == 0 && isBinarySensor) {
|
||||
measurementPeriodFactor = 14;
|
||||
} else {
|
||||
measurementPeriodFactor = 1;
|
||||
}
|
||||
int measurementPeriod = measurementPeriodBase * measurementPeriodFactor;
|
||||
long measurementPeriodMillis = TimeUnit.SECONDS.toMillis(measurementPeriod);
|
||||
long nextTransmissionAtMillis = TimeUnit.SECONDS.toMillis(protoMeasurements.getNextTransmissionAt());
|
||||
int startPoint = protoChannel.getStartPoint();
|
||||
int startTimestamp = protoChannel.getTimestamp();
|
||||
long startTimestampMillis = TimeUnit.SECONDS.toMillis(startTimestamp);
|
||||
List<Integer> sampleOffsetsList = protoChannel.getSampleOffsetsList();
|
||||
if (!CollectionUtils.isEmpty(sampleOffsetsList)) {
|
||||
int sampleOfssetsListSize = sampleOffsetsList.size();
|
||||
for (int i = 0; i < sampleOfssetsListSize; i++) {
|
||||
int sampleOffset = sampleOffsetsList.get(i);
|
||||
Integer previousSampleOffset = isBinarySensor && i > 0 ? sampleOffsetsList.get(i - 1) : null;
|
||||
if (sampleOffset == -32768) {
|
||||
log.warn("[{}],[{}] Sensor error value! Ignoring.", sessionId, sampleOffset);
|
||||
} else {
|
||||
switch (measurementType) {
|
||||
case TEMPERATURE:
|
||||
values = valuesMap.computeIfAbsent(startTimestampMillis, k ->
|
||||
CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k));
|
||||
values.addProperty("temperature_" + channel, ((double) (startPoint + sampleOffset)) / 10f);
|
||||
startTimestampMillis = startTimestampMillis + measurementPeriodMillis;
|
||||
break;
|
||||
case WATER_METER:
|
||||
values = valuesMap.computeIfAbsent(startTimestampMillis, k ->
|
||||
CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k));
|
||||
values.addProperty("pulse_counter_water_" + channel, ((double) (startPoint + sampleOffset)));
|
||||
startTimestampMillis = startTimestampMillis + measurementPeriodMillis;
|
||||
break;
|
||||
case HUMIDITY:
|
||||
values = valuesMap.computeIfAbsent(startTimestampMillis, k ->
|
||||
CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k));
|
||||
values.addProperty("humidity_" + channel, (double) (startPoint + sampleOffset));
|
||||
startTimestampMillis = startTimestampMillis + measurementPeriodMillis;
|
||||
break;
|
||||
case ATMOSPHERIC_PRESSURE:
|
||||
values = valuesMap.computeIfAbsent(startTimestampMillis, k ->
|
||||
CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k));
|
||||
values.addProperty("pressure_" + channel, (double) (startPoint + sampleOffset) / 10f);
|
||||
startTimestampMillis = startTimestampMillis + measurementPeriodMillis;
|
||||
break;
|
||||
case DIFFERENTIAL_PRESSURE:
|
||||
values = valuesMap.computeIfAbsent(startTimestampMillis, k ->
|
||||
CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k));
|
||||
values.addProperty("pressure_diff_" + channel, (double) (startPoint + sampleOffset));
|
||||
startTimestampMillis = startTimestampMillis + measurementPeriodMillis;
|
||||
break;
|
||||
case OK_ALARM:
|
||||
boolean currentIsOk = sampleOffset < 0;
|
||||
if (previousSampleOffset != null) {
|
||||
boolean previousIsOk = previousSampleOffset < 0;
|
||||
boolean isOk = previousIsOk && currentIsOk;
|
||||
boolean isAlarm = !previousIsOk && !currentIsOk;
|
||||
if (isOk || isAlarm) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
String data = currentIsOk ? "OK" : "ALARM";
|
||||
long sampleOffsetMillis = TimeUnit.SECONDS.toMillis(sampleOffset);
|
||||
long measurementTimestamp = startTimestampMillis + Math.abs(sampleOffsetMillis);
|
||||
values = valuesMap.computeIfAbsent(measurementTimestamp - 1000, k ->
|
||||
CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k));
|
||||
values.addProperty("ok_alarm_" + channel, data);
|
||||
break;
|
||||
case NO_SENSOR:
|
||||
case UNRECOGNIZED:
|
||||
log.trace("[{}][{}] Sensor error value! Ignoring.", sessionId, measurementTypeName);
|
||||
break;
|
||||
default:
|
||||
log.trace("[{}],[{}] Unsupported measurementType! Ignoring.", sessionId, measurementTypeName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.trace("[{}][{}] sampleOffsetsList list is empty!", sessionId, measurementTypeName);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("[" + sessionId + "]: Failed to get Efento measurements, reason: channels list is empty!");
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(valuesMap)) {
|
||||
List<EfentoMeasurements> efentoMeasurements = new ArrayList<>();
|
||||
for (Long ts : valuesMap.keySet()) {
|
||||
EfentoMeasurements measurement = new EfentoMeasurements(ts, valuesMap.get(ts));
|
||||
efentoMeasurements.add(measurement);
|
||||
}
|
||||
return efentoMeasurements;
|
||||
} else {
|
||||
throw new IllegalStateException("[" + sessionId + "]: Failed to collect Efento measurements, reason, values map is empty!");
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public static class EfentoMeasurements {
|
||||
|
||||
private long ts;
|
||||
private JsonObject values;
|
||||
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.efento.adaptor;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
|
||||
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||
import org.thingsboard.server.transport.coap.efento.CoapEfentoTransportResource;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class EfentoCoapAdaptor {
|
||||
|
||||
private static final Gson gson = new Gson();
|
||||
|
||||
public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, List<CoapEfentoTransportResource.EfentoMeasurements> measurements) throws AdaptorException {
|
||||
try {
|
||||
return JsonConverter.convertToTelemetryProto(gson.toJsonTree(measurements));
|
||||
} catch (Exception ex) {
|
||||
log.warn("[{}] Failed to convert EfentoMeasurements to PostTelemetry request!", sessionId);
|
||||
throw new AdaptorException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.transport.coap.efento.utils;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
|
||||
public class CoapEfentoUtils {
|
||||
|
||||
public static String convertByteArrayToString(byte[] a) {
|
||||
StringBuilder out = new StringBuilder();
|
||||
for (byte b : a) {
|
||||
out.append(String.format("%02X", b));
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
public static String convertTimestampToUtcString(long timestampInMillis) {
|
||||
String dateFormat = "yyyy-MM-dd HH:mm:ss";
|
||||
String utcZone = "UTC";
|
||||
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
|
||||
simpleDateFormat.setTimeZone(TimeZone.getTimeZone(utcZone));
|
||||
return String.format("%s UTC", simpleDateFormat.format(new Date(timestampInMillis)));
|
||||
}
|
||||
|
||||
public static JsonObject setDefaultMeasurements(String serialNumber, boolean batteryStatus, long measurementPeriod, long nextTransmissionAtMillis, long signal, long startTimestampMillis) {
|
||||
JsonObject values = new JsonObject();
|
||||
values.addProperty("serial", serialNumber);
|
||||
values.addProperty("battery", batteryStatus ? "ok" : "low");
|
||||
values.addProperty("measured_at", convertTimestampToUtcString(startTimestampMillis));
|
||||
values.addProperty("next_transmission_at", convertTimestampToUtcString(nextTransmissionAtMillis));
|
||||
values.addProperty("signal", signal);
|
||||
values.addProperty("measurement_interval", measurementPeriod);
|
||||
return values;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
syntax = "proto3";
|
||||
|
||||
option java_package = "org.thingsboard.server.gen.transport.coap";
|
||||
option java_outer_classname = "MeasurementTypeProtos";
|
||||
|
||||
enum MeasurementType {
|
||||
NO_SENSOR = 0;
|
||||
|
||||
/* [°C] - Celsius degree. Resolution 0.1°C. Range [-273.2-4000.0]. Type: Continuous */
|
||||
TEMPERATURE = 1;
|
||||
|
||||
/* [% RH] - Relative humidity. Resolution 1%. Range [0-100]. Type: Continuous */
|
||||
HUMIDITY = 2;
|
||||
|
||||
/* [hPa] - Hectopascal (1hPa = 100Pa). Resolution 0.1hPa. Range: [1.0-2000.0]. Type: Continuous */
|
||||
ATMOSPHERIC_PRESSURE = 3;
|
||||
|
||||
/* [Pa] - Pascal. Resolution 1Pa. Range [-10000-10000]Type: Continuous */
|
||||
DIFFERENTIAL_PRESSURE = 4;
|
||||
|
||||
/* Sign indicates state: (+) ALARM, (-) OK. Type: Binary */
|
||||
OK_ALARM = 5;
|
||||
|
||||
/* [IAQ] - Iaq index. Resolution 1IAQ. Range [0-500]. Sensor return also calibration status */
|
||||
/* as offset to measured value: */
|
||||
/* - offset 3000: Sensor not stabilized (always returns 25 IAQ value) */
|
||||
/* - offset 2000: Calibration required (sensor returns not accurate values) */
|
||||
/* - offset 1000: Calibration on-going (sensor returns not accurate values) */
|
||||
/* - offset 0: Calibration done (best accuracy of IAQ sensor) */
|
||||
/* Type: Continuous */
|
||||
IAQ = 6;
|
||||
|
||||
/* Sign indicates water presence: (+) water not detected, (-) water detected. Type: Binary */
|
||||
FLOODING = 7;
|
||||
|
||||
/* [NB] Number of pulses. Resolution 1 pulse. Range [0-16711679]. Type: Continuous */
|
||||
PULSE_CNT = 8;
|
||||
|
||||
/* [Wh] - Watthour; Resolution 1Wh. Range [0-16711679]. Number of Watthours in a single period. Type: Continuous */
|
||||
ELECTRICITY_METER = 9;
|
||||
|
||||
/* [l] - Liter. Resolution 1l. Range [0-16711679]. Number of litres in a single period. Type: Continuous */
|
||||
WATER_METER = 10;
|
||||
|
||||
/* [kPa] - Kilopascal (1kPa = 1000Pa); Resolution 1kPa. Range [-1000-0]. Soil moisture (tension). Type: Continuous */
|
||||
SOIL_MOISTURE = 11;
|
||||
|
||||
/* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Carbon monoxide concentration. Type: Continuous */
|
||||
CO_GAS = 12;
|
||||
|
||||
/* [ppm] - Parts per million. Resolution 0.01ppm. Range [0-1000000.00]. Nitrogen dioxide concentration. Type: Continuous*/
|
||||
NO2_GAS = 13;
|
||||
|
||||
/* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Hydrogen sulfide concentration. Type: Continuous */
|
||||
H2S_GAS = 14;
|
||||
|
||||
/* [lx] - Illuminance. Resolution 0.1lx. Range [0-100000.0]. Type: Continuous */
|
||||
AMBIENT_LIGHT = 15;
|
||||
|
||||
/* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0-1000]. */
|
||||
/* particles with an aerodynamic diameter less than 1 micrometer. Type: Continuous */
|
||||
PM_1_0 = 16; // µg/m^3
|
||||
|
||||
/* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0-1000]. */
|
||||
/* particles with an aerodynamic diameter less than 2.5 micrometers. Type: Continuous */
|
||||
PM_2_5 = 17; // µg/m^3
|
||||
|
||||
/* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0-1000]. */
|
||||
/* particles with an aerodynamic diameter less than 10 micrometers. Type: Continuous */
|
||||
PM_10_0 = 18; // µg/m^3
|
||||
|
||||
/* [dB] - Decibels. Resolution 0.1 dB. Range: [0-130.0]. Noise level. Type: Continuous */
|
||||
NOISE_LEVEL = 19; // 0.1 dB
|
||||
|
||||
/* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Ammonia concentration. Type: Continuous */
|
||||
NH3_GAS = 20;
|
||||
|
||||
/* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Methane concentration. Type: Continuous */
|
||||
CH4_GAS = 21;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
syntax = "proto3";
|
||||
import "proto_measurement_types.proto";
|
||||
|
||||
option java_package = "org.thingsboard.server.gen.transport.coap";
|
||||
option java_outer_classname = "MeasurementsProtos";
|
||||
|
||||
message ProtoChannel{
|
||||
/* Type of channel */
|
||||
MeasurementType type = 1;
|
||||
|
||||
/* Timestamp of the first sample (the oldest one) in seconds since UNIX EPOCH 01-01-1970 */
|
||||
int32 timestamp = 2;
|
||||
|
||||
/* Only used for 'Continuous' sensor types. Value used as the starting point for calculating the values of all */
|
||||
/* measurements in the package. */
|
||||
/* Format defined by 'MeasurementType' field */
|
||||
sint32 start_point = 4;
|
||||
|
||||
/* 'Continuous' sensor types */
|
||||
/* Value of the offset from the 'start_point' for each measurement in the package. The oldest sample first ([0]). */
|
||||
/* 'sample_offsets' format defined by 'MeasurementType' field. */
|
||||
/* Example: MeasurementType = 1 (temperature), start_point = 100, sample_offsets[0] = 15, sample_offsets[1] = 20 */
|
||||
/* 1st sample in the package temperature value = 11.5 °C, 2nd sample in the package temperature value = 12 °C */
|
||||
/* Calculating timestamps of the measurements: timestamp = 1606391700, measurement_period_base = 60, */
|
||||
/* measurement_period_factor = 1. Timestamp of the 1st sample = 1606391700, timestamp of the 2nd sample = 1606391760 */
|
||||
/* 'Binary' sensor types: */
|
||||
/* Absolute value of the 'sample_offsets' field indicates the offset in seconds from 'timestamp' field. */
|
||||
/* Sign (- or +) indicates the state of measurements depend of sensor type. */
|
||||
/* Value of this field equals to '1' or '-1' indicates the state at the 'timestamp'. Other values */
|
||||
/* indicate the state of the relay at the time (in seconds) equal to 'timestamp' + value. */
|
||||
/* Values of this field are incremented starting from 1 (1->0: state at the time */
|
||||
/* of 'timestamp', 2->1: state at the time equal to 'timestamp' + 1 s, 3->2 : */
|
||||
/* state at the time equal to 'timestamp' + 2 s, etc.). The first and the last sample define the time range of the */
|
||||
/* measurements. Only state changes in the time range are included in the 'sample_offsets' field */
|
||||
/* Examples: if 'timestamp' value is 1553518060 and 'sample_offsets' equals '1', it means that at 1553518060 the state */
|
||||
/* was high, if 'timestamp' value is 1553518060 and 'sample_offsets' equals '-9', it means at 1553518068 the state was low */
|
||||
repeated sint32 sample_offsets = 5 [packed=true];
|
||||
|
||||
/* Deprecated - configuration is sent to endpoint 'c' */
|
||||
//int32 lo_threshold = 6;
|
||||
|
||||
/* Deprecated - configuration is sent to endpoint 'c' */
|
||||
//int32 hi_threshold = 7;
|
||||
|
||||
/* Deprecated - configurationis sent to endpoint 'c' */
|
||||
//int32 diff_threshold = 8;
|
||||
}
|
||||
|
||||
message ProtoMeasurements {
|
||||
|
||||
/* serial number of the device */
|
||||
bytes serial_num = 1;
|
||||
|
||||
/* true - battery ok, false - battery low */
|
||||
bool battery_status = 2;
|
||||
|
||||
/* 'Measurement_period_base' and 'measurement_period_factor' define how often the measurements are taken. */
|
||||
/* Sensors of 'Continuous' type take measurement each Measurement_period_base * measurement_period_factor. */
|
||||
/* Sensors of 'Binary' type take measurement each Measurement_period_base. */
|
||||
/* For backward compatibility with versions 5.x in case of binary/mixed sensors, if the 'measurement_period_factor' is */
|
||||
/* not sent (equal to 0), then the default value '14' shall be used for period calculation. */
|
||||
/* For backward compatibility with versions 5.x in case of continues sensors, if the measurement_period_factor is */
|
||||
/* not sent (equal to 0), then the default value '1' shall be used for period calculation. */
|
||||
/* measurement period base in seconds */
|
||||
uint32 measurement_period_base = 3;
|
||||
|
||||
/* Measurement period factor */
|
||||
uint32 measurement_period_factor = 8;
|
||||
|
||||
repeated ProtoChannel channels = 4;
|
||||
|
||||
/* Timestamp of the next scheduled transmission. If the device will not send data until this time, */
|
||||
/* it should be considered as 'lost' */
|
||||
uint32 next_transmission_at = 5;
|
||||
|
||||
/* reason of transmission - unsigned integer where each bit indicates different */
|
||||
/* possible communication reason. Can be more than one */
|
||||
/* - bit 0: first message after sensor reset */
|
||||
/* - bit 1: user button triggered */
|
||||
/* - bit 2: user BLE triggered */
|
||||
/* - bit 3-7: number of retries -> incremented after each unsuccessful transmission. Max value 4. */
|
||||
/* Set to 0 after a successful transmission. */
|
||||
/* - bit 8: channel 1 lower threshold exceeded */
|
||||
/* - bit 9: channel 1 lower threshold returned */
|
||||
/* - bit 10: channel 1 higher threshold exceeded */
|
||||
/* - bit 11: channel 1 higher threshold returned */
|
||||
/* - bit 12: channel 1 differential threshold crossed */
|
||||
/* - bits 13-17: channel 2 thresholds (same as for channel 1) */
|
||||
/* - bits 18-22: channel 3 thresholds (same as for channel 1) */
|
||||
/* - bits 23-27: channel 4 or 5 or 6 thresholds (same as for channel 1) */
|
||||
uint32 transfer_reason = 6;
|
||||
|
||||
/* Signal strength level mapped from RSSI */
|
||||
/* - 0 : 113 dBm or less */
|
||||
/* - 1 : 111 dBm */
|
||||
/* - 2...30 : 109...-53 dBm */
|
||||
/* - 31 : -51 dBm or greater */
|
||||
/* - 99 : Not known or not detectable */
|
||||
uint32 signal = 7;
|
||||
|
||||
/* Hash of the current configuration. Hash value changes each time a device receives a new configuration */
|
||||
uint32 hash = 9;
|
||||
|
||||
/* Optional string up to 36 bytes long. Can be set to any user define value or hold device's IMEI */
|
||||
string cloud_token = 16;
|
||||
}
|
||||
Reference in new issue
Block a user