first-fix

This commit is contained in:
Alexander Abakumov committed 2024-06-23 23:04:49 +03:00
1 parent 6d87c1edc8
commit 565d9d673a
6284 files changed
+820267 -76

No files matched your search

+108
View File
@@ -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>
@@ -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);
}
}
@@ -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<>();
}
@@ -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);
}
}
}
@@ -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;
}
}
@@ -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));
}
}
}
@@ -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() {
}
}
@@ -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;
}
}
@@ -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;
}
}
}
@@ -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();
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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));
}
}
}
@@ -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);
}
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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());
}
}
}
@@ -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());
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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;
}