Repository: eclipse/leshan
Pull request #876 (2020-08-19 15:44:12)

Add LowerLayerSetter class to allow to tweak coap level parameter for requests
This aims to implements #875.

An example of use : 
```java
// configure reliability parameters
Builder reliabilityParams = ReliabilityLayerParameters.builder();
reliabilityParams.ackRandomFactor(1).ackTimeout(100).ackTimeoutScale(1).maxRetransmit(1);
// send requests
ReadResponse response = server.send(registration,
                                    new ReadRequest("3/0/1"),  
                                    CoapRequestSetter.reliabilitySetter(reliabilityParams), 
                                    timeout);
``` 

sbernard31: The master version (without any backward compatibility stuff and with support for bootstrap server is available at https://github.com/eclipse/leshan/commit/7a2966a3c59b89db1aa832d42ab7191afb9e6ff6)

Patch:
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServer.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServer.java
index 32618f3a..2bcc7b50 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServer.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServer.java
@@ -68,7 +68,9 @@ import org.eclipse.leshan.server.registration.RegistrationService;
 import org.eclipse.leshan.server.registration.RegistrationServiceImpl;
 import org.eclipse.leshan.server.registration.RegistrationStore;
 import org.eclipse.leshan.server.registration.RegistrationUpdate;
+import org.eclipse.leshan.server.request.LowerLayerConfig;
 import org.eclipse.leshan.server.request.LwM2mRequestSender;
+import org.eclipse.leshan.server.request.LwM2mRequestSender2;
 import org.eclipse.leshan.server.security.Authorizer;
 import org.eclipse.leshan.server.security.EditableSecurityStore;
 import org.eclipse.leshan.server.security.SecurityInfo;
@@ -87,6 +89,7 @@ import org.slf4j.LoggerFactory;
  * <p>
  * The {@link LeshanServerBuilder} should be the preferred way to build an instance of {@link LeshanServer}.
  */
+@SuppressWarnings("deprecation")
 public class LeshanServer {
 
     private static final Logger LOG = LoggerFactory.getLogger(LeshanServer.class);
@@ -474,7 +477,7 @@ public class LeshanServer {
      */
     public <T extends LwM2mResponse> T send(Registration destination, DownlinkRequest<T> request)
             throws InterruptedException {
-        return requestSender.send(destination, request, DEFAULT_TIMEOUT);
+        return send(destination, request, DEFAULT_TIMEOUT);
     }
 
     /**
@@ -500,7 +503,42 @@ public class LeshanServer {
      */
     public <T extends LwM2mResponse> T send(Registration destination, DownlinkRequest<T> request, long timeoutInMs)
             throws InterruptedException {
-        return requestSender.send(destination, request, timeoutInMs);
+        return send(destination, request, null, timeoutInMs);
+    }
+
+    /**
+     * Send a Lightweight M2M request synchronously. Will block until a response is received from the remote server.
+     * <p>
+     * The synchronous way could block a thread during a long time so it is more recommended to use the asynchronous
+     * way.
+     * 
+     * @param destination The {@link Registration} associate to the device we want to sent the request.
+     * @param request The request to send to the client.
+     * @param lowerLayerConfig to tweak lower layer request (e.g. coap request)
+     * @param timeoutInMs The global timeout to wait in milliseconds (see
+     *        https://github.com/eclipse/leshan/wiki/Request-Timeout)
+     * @return the LWM2M response. The response can be <code>null</code> if the timeout expires (see
+     *         https://github.com/eclipse/leshan/wiki/Request-Timeout).
+     * 
+     * @throws CodecException if request payload can not be encoded.
+     * @throws InterruptedException if the thread was interrupted.
+     * @throws RequestRejectedException if the request is rejected by foreign peer.
+     * @throws RequestCanceledException if the request is cancelled.
+     * @throws SendFailedException if the request can not be sent. E.g. error at CoAP or DTLS/UDP layer.
+     * @throws InvalidResponseException if the response received is malformed.
+     * @throws ClientSleepingException if client is currently sleeping.
+     */
+    public <T extends LwM2mResponse> T send(Registration destination, DownlinkRequest<T> request,
+            LowerLayerConfig lowerLayerConfig, long timeoutInMs) throws InterruptedException {
+        if (requestSender instanceof LwM2mRequestSender2) {
+            return ((LwM2mRequestSender2) requestSender).send(destination, request, lowerLayerConfig, timeoutInMs);
+        } else {
+            if (lowerLayerConfig != null) {
+                LOG.warn("{} should implement the new interface LwM2mRequestSender2",
+                        requestSender.getClass().getCanonicalName());
+            }
+            return requestSender.send(destination, request, timeoutInMs);
+        }
     }
 
     /**
@@ -533,7 +571,7 @@ public class LeshanServer {
      */
     public <T extends LwM2mResponse> void send(Registration destination, DownlinkRequest<T> request,
             ResponseCallback<T> responseCallback, ErrorCallback errorCallback) {
-        requestSender.send(destination, request, DEFAULT_TIMEOUT, responseCallback, errorCallback);
+        send(destination, request, DEFAULT_TIMEOUT, responseCallback, errorCallback);
     }
 
     /**
@@ -565,7 +603,52 @@ public class LeshanServer {
      */
     public <T extends LwM2mResponse> void send(Registration destination, DownlinkRequest<T> request, long timeoutInMs,
             ResponseCallback<T> responseCallback, ErrorCallback errorCallback) {
-        requestSender.send(destination, request, timeoutInMs, responseCallback, errorCallback);
+        send(destination, request, null, timeoutInMs, responseCallback, errorCallback);
+    }
+
+    /**
+     * Send a Lightweight M2M {@link DownlinkRequest} asynchronously to a LWM2M client.
+     * <p>
+     * The Californium API does not ensure that message callback are exclusive. E.g. In some race condition, you can get
+     * a onReponse call and a onCancel one. This method ensures that you will receive only one event. Meaning, you get
+     * either 1 response or 1 error.
+     * 
+     * @param destination The {@link Registration} associate to the device we want to sent the request.
+     * @param request The request to send to the client.
+     * @param lowerLayerConfig to tweak lower layer request (e.g. coap request)
+     * @param timeoutInMs The global timeout to wait in milliseconds (see
+     *        https://github.com/eclipse/leshan/wiki/Request-Timeout)
+     * @param responseCallback a callback called when a response is received (successful or error response). This
+     *        callback MUST NOT be null.
+     * @param errorCallback a callback called when an error or exception occurred when response is received. It can be :
+     *        <ul>
+     *        <li>{@link RequestRejectedException} if the request is rejected by foreign peer.</li>
+     *        <li>{@link RequestCanceledException} if the request is cancelled.</li>
+     *        <li>{@link SendFailedException} if the request can not be sent. E.g. error at CoAP or DTLS/UDP layer.</li>
+     *        <li>{@link InvalidResponseException} if the response received is malformed.</li>
+     *        <li>{@link ClientSleepingException} if client is currently sleeping.</li>
+     *        <li>{@link TimeoutException} if the timeout expires (see
+     *        https://github.com/eclipse/leshan/wiki/Request-Timeout).</li>
+     *        <li>or any other RuntimeException for unexpected issue.
+     *        </ul>
+     *        This callback MUST NOT be null.
+     * @throws CodecException if request payload can not be encoded.
+     * 
+     * @since 1.2
+     */
+    public <T extends LwM2mResponse> void send(Registration destination, DownlinkRequest<T> request,
+            LowerLayerConfig lowerLayerConfig, long timeoutInMs, ResponseCallback<T> responseCallback,
+            ErrorCallback errorCallback) {
+        if (requestSender instanceof LwM2mRequestSender2) {
+            ((LwM2mRequestSender2) requestSender).send(destination, request, lowerLayerConfig, timeoutInMs,
+                    responseCallback, errorCallback);
+        } else {
+            if (lowerLayerConfig != null) {
+                LOG.warn("{} should implement the new interface LwM2mRequestSender2",
+                        requestSender.getClass().getCanonicalName());
+            }
+            requestSender.send(destination, request, timeoutInMs, responseCallback, errorCallback);
+        }
     }
 
     /**
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/CaliforniumLwM2mBootstrapRequestSender.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/CaliforniumLwM2mBootstrapRequestSender.java
index 906c34b7..81b6f290 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/CaliforniumLwM2mBootstrapRequestSender.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/CaliforniumLwM2mBootstrapRequestSender.java
@@ -84,7 +84,7 @@ public class CaliforniumLwM2mBootstrapRequestSender implements LwM2mBootstrapReq
     public <T extends LwM2mResponse> T send(BootstrapSession destination, DownlinkRequest<T> request, long timeoutInMs)
             throws InterruptedException {
         return sender.sendLwm2mRequest(destination.getEndpoint(), destination.getIdentity(), destination.getId(), model,
-                null, request, timeoutInMs, false);
+                null, request, null, timeoutInMs, false);
     }
 
     /**
@@ -117,7 +117,7 @@ public class CaliforniumLwM2mBootstrapRequestSender implements LwM2mBootstrapReq
     public <T extends LwM2mResponse> void send(BootstrapSession destination, DownlinkRequest<T> request,
             long timeoutInMs, ResponseCallback<T> responseCallback, ErrorCallback errorCallback) {
         sender.sendLwm2mRequest(destination.getEndpoint(), destination.getIdentity(), destination.getId(), model, null,
-                request, timeoutInMs, responseCallback, errorCallback, false);
+                request, null, timeoutInMs, responseCallback, errorCallback, false);
     }
 
     /**
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CaliforniumLwM2mRequestSender.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CaliforniumLwM2mRequestSender.java
index 8c16188c..ba8bec90 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CaliforniumLwM2mRequestSender.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CaliforniumLwM2mRequestSender.java
@@ -41,12 +41,13 @@ import org.eclipse.leshan.server.Destroyable;
 import org.eclipse.leshan.server.californium.observation.ObservationServiceImpl;
 import org.eclipse.leshan.server.model.LwM2mModelProvider;
 import org.eclipse.leshan.server.registration.Registration;
-import org.eclipse.leshan.server.request.LwM2mRequestSender;
+import org.eclipse.leshan.server.request.LowerLayerConfig;
+import org.eclipse.leshan.server.request.LwM2mRequestSender2;
 
 /**
- * An implementation of {@link LwM2mRequestSender} and {@link CoapRequestSender} based on Californium.
+ * An implementation of {@link LwM2mRequestSender2} and {@link CoapRequestSender} based on Californium.
  */
-public class CaliforniumLwM2mRequestSender implements LwM2mRequestSender, CoapRequestSender, Destroyable {
+public class CaliforniumLwM2mRequestSender implements LwM2mRequestSender2, CoapRequestSender, Destroyable {
 
     private final ObservationServiceImpl observationService;
     private final LwM2mModelProvider modelProvider;
@@ -95,13 +96,42 @@ public class CaliforniumLwM2mRequestSender implements LwM2mRequestSender, CoapRe
     @Override
     public <T extends LwM2mResponse> T send(Registration destination, DownlinkRequest<T> request, long timeoutInMs)
             throws InterruptedException {
+        return send(destination, request, null, timeoutInMs);
+    }
+
+    /**
+     * Send a Lightweight M2M request synchronously. Will block until a response is received from the remote server.
+     * <p>
+     * The synchronous way could block a thread during a long time so it is more recommended to use the asynchronous
+     * way.
+     * 
+     * @param destination The {@link Registration} associate to the device we want to sent the request.
+     * @param request The request to send to the client.
+     * @param lowerLayerConfig to tweak lower layer request (e.g. coap request)
+     * @param timeoutInMs The global timeout to wait in milliseconds (see
+     *        https://github.com/eclipse/leshan/wiki/Request-Timeout)
+     * @return the LWM2M response. The response can be <code>null</code> if the timeout expires (see
+     *         https://github.com/eclipse/leshan/wiki/Request-Timeout).
+     * 
+     * @throws CodecException if request payload can not be encoded.
+     * @throws InterruptedException if the thread was interrupted.
+     * @throws RequestRejectedException if the request is rejected by foreign peer.
+     * @throws RequestCanceledException if the request is cancelled.
+     * @throws SendFailedException if the request can not be sent. E.g. error at CoAP or DTLS/UDP layer.
+     * @throws InvalidResponseException if the response received is malformed.
+     * @throws UnconnectedPeerException if client is not connected (no dtls connection available).
+     */
+    @Override
+    public <T extends LwM2mResponse> T send(Registration destination, DownlinkRequest<T> request,
+            LowerLayerConfig lowerLayerConfig, long timeoutInMs) throws InterruptedException {
 
         // Retrieve the objects definition
         final LwM2mModel model = modelProvider.getObjectModel(destination);
 
         // Send requests synchronously
         T response = sender.sendLwm2mRequest(destination.getEndpoint(), destination.getIdentity(), destination.getId(),
-                model, destination.getRootPath(), request, timeoutInMs, destination.canInitiateConnection());
+                model, destination.getRootPath(), request, lowerLayerConfig, timeoutInMs,
+                destination.canInitiateConnection());
 
         // Handle special observe case
         if (response != null && response.getClass() == ObserveResponse.class && response.isSuccess()) {
@@ -140,12 +170,47 @@ public class CaliforniumLwM2mRequestSender implements LwM2mRequestSender, CoapRe
     @Override
     public <T extends LwM2mResponse> void send(final Registration destination, DownlinkRequest<T> request,
             long timeoutInMs, final ResponseCallback<T> responseCallback, ErrorCallback errorCallback) {
+        send(destination, request, null, timeoutInMs, responseCallback, errorCallback);
+    }
+
+    /**
+     * Send a Lightweight M2M {@link DownlinkRequest} asynchronously to a LWM2M client.
+     * 
+     * The Californium API does not ensure that message callback are exclusive. E.g. In some race condition, you can get
+     * a onReponse call and a onCancel one. This method ensures that you will receive only one event. Meaning, you get
+     * either 1 response or 1 error.
+     * 
+     * @param destination The {@link Registration} associate to the device we want to sent the request.
+     * @param request The request to send to the client.
+     * @param lowerLayerConfig to tweak lower layer request (e.g. coap request)
+     * @param timeoutInMs The global timeout to wait in milliseconds (see
+     *        https://github.com/eclipse/leshan/wiki/Request-Timeout)
+     * @param responseCallback a callback called when a response is received (successful or error response). This
+     *        callback MUST NOT be null.
+     * @param errorCallback a callback called when an error or exception occurred when response is received. It can be :
+     *        <ul>
+     *        <li>{@link RequestRejectedException} if the request is rejected by foreign peer.</li>
+     *        <li>{@link RequestCanceledException} if the request is cancelled.</li>
+     *        <li>{@link SendFailedException} if the request can not be sent. E.g. error at CoAP or DTLS/UDP layer.</li>
+     *        <li>{@link InvalidResponseException} if the response received is malformed.</li>
+     *        <li>{@link UnconnectedPeerException} if client is not connected (no dtls connection available).</li>
+     *        <li>{@link TimeoutException} if the timeout expires (see
+     *        https://github.com/eclipse/leshan/wiki/Request-Timeout).</li>
+     *        <li>or any other RuntimeException for unexpected issue.
+     *        </ul>
+     *        This callback MUST NOT be null.
+     * @throws CodecException if request payload can not be encoded.
+     */
+    @Override
+    public <T extends LwM2mResponse> void send(final Registration destination, DownlinkRequest<T> request,
+            LowerLayerConfig lowerLayerConfig, long timeoutInMs, final ResponseCallback<T> responseCallback,
+            ErrorCallback errorCallback) {
         // Retrieve the objects definition
         final LwM2mModel model = modelProvider.getObjectModel(destination);
 
         // Send requests asynchronously
         sender.sendLwm2mRequest(destination.getEndpoint(), destination.getIdentity(), destination.getId(), model,
-                destination.getRootPath(), request, timeoutInMs, new ResponseCallback<T>() {
+                destination.getRootPath(), request, lowerLayerConfig, timeoutInMs, new ResponseCallback<T>() {
                     @Override
                     public void onResponse(T response) {
                         if (response != null && response.getClass() == ObserveResponse.class && response.isSuccess()) {
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CaliforniumQueueModeRequestSender.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CaliforniumQueueModeRequestSender.java
index 2ab41eee..be518d6a 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CaliforniumQueueModeRequestSender.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CaliforniumQueueModeRequestSender.java
@@ -26,10 +26,12 @@ import org.eclipse.leshan.server.queue.PresenceServiceImpl;
 import org.eclipse.leshan.server.queue.QueueModeLwM2mRequestSender;
 import org.eclipse.leshan.server.registration.Registration;
 import org.eclipse.leshan.server.request.LwM2mRequestSender;
+import org.eclipse.leshan.server.request.LwM2mRequestSender2;
 
 /**
- * A {@link LwM2mRequestSender} and {@link CoapRequestSender} which supports LWM2M Queue Mode.
+ * A {@link LwM2mRequestSender2} and {@link CoapRequestSender} which supports LWM2M Queue Mode.
  */
+@SuppressWarnings("deprecation")
 public class CaliforniumQueueModeRequestSender extends QueueModeLwM2mRequestSender
         implements CoapRequestSender, Destroyable {
 
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java
index b8ce736b..ce0a1809 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java
@@ -47,6 +47,7 @@ import org.eclipse.leshan.core.request.WriteAttributesRequest;
 import org.eclipse.leshan.core.request.WriteRequest;
 import org.eclipse.leshan.core.util.StringUtils;
 import org.eclipse.leshan.server.californium.observation.ObserveUtil;
+import org.eclipse.leshan.server.request.LowerLayerConfig;
 
 /**
  * This class is able to create CoAP request from LWM2M {@link DownlinkRequest}.
@@ -67,6 +68,9 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor2 {
     private final LwM2mModel model;
     private final LwM2mNodeEncoder encoder;
 
+    private LowerLayerConfig lowerLayerConfig;
+
+    @Deprecated
     public CoapRequestBuilder(Identity destination, String rootPath, String registrationId, String endpoint,
             LwM2mModel model, LwM2mNodeEncoder encoder, boolean allowConnectionInitiation) {
         this.destination = destination;
@@ -78,12 +82,23 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor2 {
         this.allowConnectionInitiation = allowConnectionInitiation;
     }
 
+    /**
+     * @since 1.2
+     */
+    public CoapRequestBuilder(Identity destination2, String rootPath2, String sessionId, String endpointName,
+            LwM2mModel model2, LwM2mNodeEncoder encoder2, boolean allowConnectionInitiation2,
+            LowerLayerConfig lowerLayerConfig) {
+        this(destination2, rootPath2, sessionId, endpointName, model2, encoder2, allowConnectionInitiation2);
+        this.lowerLayerConfig = lowerLayerConfig;
+    }
+
     @Override
     public void visit(ReadRequest request) {
         coapRequest = Request.newGet();
         if (request.getContentFormat() != null)
             coapRequest.getOptions().setAccept(request.getContentFormat().getCode());
         setTarget(coapRequest, request.getPath());
+        applyLowerLayerConfig(coapRequest);
     }
 
     @Override
@@ -91,6 +106,7 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor2 {
         coapRequest = Request.newGet();
         setTarget(coapRequest, request.getPath());
         coapRequest.getOptions().setAccept(MediaTypeRegistry.APPLICATION_LINK_FORMAT);
+        applyLowerLayerConfig(coapRequest);
     }
 
     @Override
@@ -100,6 +116,7 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor2 {
         coapRequest.getOptions().setContentFormat(format.getCode());
         coapRequest.setPayload(encoder.encode(request.getNode(), format, request.getPath(), model));
         setTarget(coapRequest, request.getPath());
+        applyLowerLayerConfig(coapRequest);
     }
 
     @Override
@@ -109,6 +126,7 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor2 {
         for (String query : request.getAttributes().toQueryParams()) {
             coapRequest.getOptions().addUriQuery(query);
         }
+        applyLowerLayerConfig(coapRequest);
     }
 
     @Override
@@ -119,6 +137,7 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor2 {
             coapRequest.setPayload(request.getParameters());
             coapRequest.getOptions().setContentFormat(MediaTypeRegistry.TEXT_PLAIN);
         }
+        applyLowerLayerConfig(coapRequest);
     }
 
     @Override
@@ -134,12 +153,14 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor2 {
         }
         coapRequest.setPayload(encoder.encode(node, request.getContentFormat(), request.getPath(), model));
         setTarget(coapRequest, request.getPath());
+        applyLowerLayerConfig(coapRequest);
     }
 
     @Override
     public void visit(DeleteRequest request) {
         coapRequest = Request.newDelete();
         setTarget(coapRequest, request.getPath());
+        applyLowerLayerConfig(coapRequest);
     }
 
     @Override
@@ -152,6 +173,7 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor2 {
 
         // add context info to the observe request
         coapRequest.setUserContext(ObserveUtil.createCoapObserveRequestContext(endpoint, registrationId, request));
+        applyLowerLayerConfig(coapRequest);
     }
 
     @Override
@@ -162,6 +184,7 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor2 {
         if (request.getObservation().getContentFormat() != null)
             coapRequest.getOptions().setAccept(request.getObservation().getContentFormat().getCode());
         setTarget(coapRequest, request.getPath());
+        applyLowerLayerConfig(coapRequest);
     }
 
     @Override
@@ -172,6 +195,7 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor2 {
         coapRequest.getOptions().setContentFormat(format.getCode());
         coapRequest.setPayload(encoder.encode(request.getNode(), format, request.getPath(), model));
         setTarget(coapRequest, request.getPath());
+        applyLowerLayerConfig(coapRequest);
     }
 
     @Override
@@ -179,6 +203,7 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor2 {
         coapRequest = Request.newGet();
         setTarget(coapRequest, request.getPath());
         coapRequest.getOptions().setAccept(MediaTypeRegistry.APPLICATION_LINK_FORMAT);
+        applyLowerLayerConfig(coapRequest);
     }
 
     @Override
@@ -188,6 +213,7 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor2 {
         EndpointContext context = EndpointContextUtil.extractContext(destination, allowConnectionInitiation);
         coapRequest.setDestinationContext(context);
         setTarget(coapRequest, request.getPath());
+        applyLowerLayerConfig(coapRequest);
     }
 
     @Override
@@ -207,6 +233,7 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor2 {
         }
 
         coapRequest.getOptions().addUriPath("bs");
+        applyLowerLayerConfig(coapRequest);
     }
 
     protected void setTarget(Request coapRequest, LwM2mPath path) {
@@ -242,6 +269,11 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor2 {
         }
     }
 
+    protected void applyLowerLayerConfig(Request coapRequest) {
+        if (lowerLayerConfig != null)
+            lowerLayerConfig.apply(coapRequest);
+    }
+
     public Request getRequest() {
         return coapRequest;
     }
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestConfig.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestConfig.java
new file mode 100644
index 00000000..2a45abb8
--- /dev/null
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestConfig.java
@@ -0,0 +1,65 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.californium.request;
+
+import org.eclipse.californium.core.coap.Request;
+import org.eclipse.californium.core.network.stack.ReliabilityLayerParameters;
+import org.eclipse.leshan.server.request.LowerLayerConfig;
+
+public class CoapRequestConfig implements LowerLayerConfig {
+
+    private final ReliabilityLayerParameters reliabilityLayerParameters;
+
+    private CoapRequestConfig(ReliabilityLayerParameters reliabilityLayerParameters) {
+        this.reliabilityLayerParameters = reliabilityLayerParameters;
+    }
+
+    @Override
+    public void apply(Object lowerRequest) {
+        if (lowerRequest instanceof Request) {
+            apply((Request) lowerRequest);
+        }
+    }
+
+    protected void apply(Request coapRequest) {
+        if (reliabilityLayerParameters != null)
+            coapRequest.setReliabilityLayerParameters(reliabilityLayerParameters);
+    }
+
+    public static CoapRequestConfig reliabilityConfig(ReliabilityLayerParameters.Builder reliabilityParametersBuilder) {
+        return builder().setReliabilityLayerParameters(reliabilityParametersBuilder.build()).build();
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public static class Builder {
+        private ReliabilityLayerParameters reliabilityLayerParameters;
+
+        private Builder() {
+        }
+
+        public Builder setReliabilityLayerParameters(ReliabilityLayerParameters reliabilityLayerParameters) {
+            this.reliabilityLayerParameters = reliabilityLayerParameters;
+            return this;
+        }
+
+        public CoapRequestConfig build() {
+            return new CoapRequestConfig(reliabilityLayerParameters);
+        }
+    }
+}
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/RequestSender.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/RequestSender.java
index 8778c1fe..d1f50a28 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/RequestSender.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/RequestSender.java
@@ -54,6 +54,7 @@ import org.eclipse.leshan.core.response.ResponseCallback;
 import org.eclipse.leshan.core.util.NamedThreadFactory;
 import org.eclipse.leshan.core.util.Validate;
 import org.eclipse.leshan.server.Destroyable;
+import org.eclipse.leshan.server.request.LowerLayerConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -120,14 +121,57 @@ public class RequestSender implements Destroyable {
      * @throws SendFailedException if the request can not be sent. E.g. error at CoAP or DTLS/UDP layer.
      * @throws UnconnectedPeerException if client is not connected (no dtls connection available).
      * @throws InvalidResponseException if the response received is malformed.
+     * 
+     * @deprecated use
+     *             {@link #sendLwm2mRequest(String, Identity, String, LwM2mModel, String, DownlinkRequest, LowerLayerConfig, long, boolean)}
+     *             instead
      */
+    @Deprecated
     public <T extends LwM2mResponse> T sendLwm2mRequest(final String endpointName, Identity destination,
             String sessionId, final LwM2mModel model, String rootPath, final DownlinkRequest<T> request,
             long timeoutInMs, boolean allowConnectionInitiation) throws InterruptedException {
+        return sendLwm2mRequest(endpointName, destination, sessionId, model, rootPath, request, null, timeoutInMs,
+                allowConnectionInitiation);
+    }
+
+    /**
+     * Sends a Lightweight M2M {@link DownlinkRequest} synchronously to a LWM2M client. Will block until a response is
+     * received from the remote client.
+     * <p>
+     * The synchronous way could block a thread during a long time so it is more recommended to use the asynchronous
+     * way.
+     * 
+     * @param endpointName the LWM2M client endpoint name.
+     * @param destination the LWM2M client {@link Identity}.
+     * @param sessionId A session Identifier which could be reused to cancel all ongoing request related to this
+     *        sessionId. See {@link #cancelRequests(String)}.
+     * @param model The {@link LwM2mModel} used to encode payload in request and decode payload in response.
+     * @param rootPath a rootpath to prefix to the LWM2M path to create the CoAP path. (see 8.2.2 Alternate Path in
+     *        LWM2M specification)
+     * @param request The request to send to the client.
+     * @param lowerLayerConfig to tweak lower layer request (e.g. coap request)
+     * @param timeoutInMs The response timeout to wait in milliseconds (see
+     *        https://github.com/eclipse/leshan/wiki/Request-Timeout)
+     * @param allowConnectionInitiation This request can initiate a Handshake if there is no DTLS connection.
+     * @return the response or <code>null</code> if the timeout expires (see
+     *         https://github.com/eclipse/leshan/wiki/Request-Timeout).
+     * 
+     * @throws CodecException if request payload can not be encoded.
+     * @throws InterruptedException if the thread was interrupted.
+     * @throws RequestRejectedException if the request is rejected by foreign peer.
+     * @throws RequestCanceledException if the request is cancelled.
+     * @throws SendFailedException if the request can not be sent. E.g. error at CoAP or DTLS/UDP layer.
+     * @throws UnconnectedPeerException if client is not connected (no dtls connection available).
+     * @throws InvalidResponseException if the response received is malformed.
+     */
+    public <T extends LwM2mResponse> T sendLwm2mRequest(final String endpointName, Identity destination,
+            String sessionId, final LwM2mModel model, String rootPath, final DownlinkRequest<T> request,
+            LowerLayerConfig lowerLayerConfig, long timeoutInMs, boolean allowConnectionInitiation)
+            throws InterruptedException {
 
         // Create the CoAP request from LwM2m request
         CoapRequestBuilder coapClientRequestBuilder = new CoapRequestBuilder(destination, rootPath, sessionId,
-                endpointName, model, encoder, allowConnectionInitiation);
+                endpointName, model, encoder, allowConnectionInitiation, lowerLayerConfig);
         request.accept(coapClientRequestBuilder);
         final Request coapRequest = coapClientRequestBuilder.getRequest();
 
@@ -190,18 +234,68 @@ public class RequestSender implements Destroyable {
      *        This callback MUST NOT be null.
      * @param allowConnectionInitiation This request can initiate a Handshake if there is no DTLS connection.
      * @throws CodecException if request payload can not be encoded.
+     * 
+     * @deprecated use
+     *             {@link #sendLwm2mRequest(String, Identity, String, LwM2mModel, String, DownlinkRequest, LowerLayerConfig, long, ResponseCallback, ErrorCallback, boolean)}
+     *             instead
      */
+    @Deprecated
     public <T extends LwM2mResponse> void sendLwm2mRequest(final String endpointName, Identity destination,
             String sessionId, final LwM2mModel model, String rootPath, final DownlinkRequest<T> request,
             long timeoutInMs, ResponseCallback<T> responseCallback, ErrorCallback errorCallback,
             boolean allowConnectionInitiation) {
+        sendLwm2mRequest(endpointName, destination, sessionId, model, rootPath, request, null, timeoutInMs,
+                responseCallback, errorCallback, allowConnectionInitiation);
+        ;
+    }
+
+    /**
+     * Send a Lightweight M2M {@link DownlinkRequest} asynchronously to a LWM2M client.
+     * 
+     * The Californium API does not ensure that message callback are exclusive. E.g. In some race condition, you can get
+     * a onReponse call and a onCancel one. This method ensures that you will receive only one event. Meaning, you get
+     * either 1 response or 1 error.
+     * 
+     * @param endpointName the LWM2M client endpoint name.
+     * @param destination the LWM2M client {@link Identity}.
+     * @param sessionId A session Identifier which could be reused to cancel all ongoing request related to this
+     *        sessionId. See {@link #cancelRequests(String)}.
+     * @param model The {@link LwM2mModel} used to encode payload in request and decode payload in response.
+     * @param rootPath a rootpath to prefix to the LWM2M path to create the CoAP path. (see 8.2.2 Alternate Path in
+     *        LWM2M specification)
+     * @param request The request to send to the client.
+     * @param lowerLayerConfig to tweak lower layer request (e.g. coap request)
+     * @param timeoutInMs The response timeout to wait in milliseconds (see
+     *        https://github.com/eclipse/leshan/wiki/Request-Timeout)
+     * @param responseCallback a callback called when a response is received (successful or error response). This
+     *        callback MUST NOT be null.
+     * @param errorCallback a callback called when an error or exception occurred when response is received. It can be :
+     *        <ul>
+     *        <li>{@link RequestRejectedException} if the request is rejected by foreign peer.</li>
+     *        <li>{@link RequestCanceledException} if the request is cancelled.</li>
+     *        <li>{@link SendFailedException} if the request can not be sent. E.g. error at CoAP or DTLS/UDP layer.</li>
+     *        <li>{@link InvalidResponseException} if the response received is malformed.</li>
+     *        <li>{@link UnconnectedPeerException} if client is not connected (no dtls connection available).</li>
+     *        <li>{@link TimeoutException} if the timeout expires (see
+     *        https://github.com/eclipse/leshan/wiki/Request-Timeout).</li>
+     *        <li>or any other RuntimeException for unexpected issue.
+     *        </ul>
+     *        This callback MUST NOT be null.
+     * @param allowConnectionInitiation This request can initiate a Handshake if there is no DTLS connection.
+     * @throws CodecException if request payload can not be encoded.
+     */
+
+    public <T extends LwM2mResponse> void sendLwm2mRequest(final String endpointName, Identity destination,
+            String sessionId, final LwM2mModel model, String rootPath, final DownlinkRequest<T> request,
+            LowerLayerConfig lowerLayerConfig, long timeoutInMs, ResponseCallback<T> responseCallback,
+            ErrorCallback errorCallback, boolean allowConnectionInitiation) {
 
         Validate.notNull(responseCallback);
         Validate.notNull(errorCallback);
 
         // Create the CoAP request from LwM2m request
         CoapRequestBuilder coapClientRequestBuilder = new CoapRequestBuilder(destination, rootPath, sessionId,
-                endpointName, model, encoder, allowConnectionInitiation);
+                endpointName, model, encoder, allowConnectionInitiation, lowerLayerConfig);
         request.accept(coapClientRequestBuilder);
         final Request coapRequest = coapClientRequestBuilder.getRequest();
 
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/QueueModeLwM2mRequestSender.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/QueueModeLwM2mRequestSender.java
index 2104c038..b4c0f41a 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/QueueModeLwM2mRequestSender.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/QueueModeLwM2mRequestSender.java
@@ -24,12 +24,15 @@ import org.eclipse.leshan.core.response.LwM2mResponse;
 import org.eclipse.leshan.core.response.ResponseCallback;
 import org.eclipse.leshan.core.util.Validate;
 import org.eclipse.leshan.server.registration.Registration;
+import org.eclipse.leshan.server.request.LowerLayerConfig;
 import org.eclipse.leshan.server.request.LwM2mRequestSender;
+import org.eclipse.leshan.server.request.LwM2mRequestSender2;
 
 /**
  * A {@link LwM2mRequestSender} which supports LWM2M Queue Mode.
  */
-public class QueueModeLwM2mRequestSender implements LwM2mRequestSender {
+@SuppressWarnings("deprecation")
+public class QueueModeLwM2mRequestSender implements LwM2mRequestSender2 {
 
     protected PresenceServiceImpl presenceService;
     protected LwM2mRequestSender delegatedSender;
@@ -54,9 +57,23 @@ public class QueueModeLwM2mRequestSender implements LwM2mRequestSender {
     public <T extends LwM2mResponse> T send(final Registration destination, DownlinkRequest<T> request, long timeout)
             throws InterruptedException {
 
+        return send(destination, request, null, timeout);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public <T extends LwM2mResponse> T send(final Registration destination, DownlinkRequest<T> request,
+            LowerLayerConfig lowerLayerConfig, long timeout) throws InterruptedException {
+
         // If the client does not use Q-Mode, just send
         if (!destination.usesQueueMode()) {
-            return delegatedSender.send(destination, request, timeout);
+            if (delegatedSender instanceof LwM2mRequestSender2) {
+                return ((LwM2mRequestSender2) delegatedSender).send(destination, request, lowerLayerConfig, timeout);
+            } else {
+                return delegatedSender.send(destination, request, timeout);
+            }
         }
 
         // If the client uses Q-Mode...
@@ -68,7 +85,13 @@ public class QueueModeLwM2mRequestSender implements LwM2mRequestSender {
 
         // Use delegation to send the request
         try {
-            T response = delegatedSender.send(destination, request, timeout);
+            T response = null;
+            if (delegatedSender instanceof LwM2mRequestSender2) {
+                response = ((LwM2mRequestSender2) delegatedSender).send(destination, request, lowerLayerConfig,
+                        timeout);
+            } else {
+                response = delegatedSender.send(destination, request, timeout);
+            }
             if (response != null) {
                 // Set the client awake. This will restart the timer.
                 presenceService.setAwake(destination);
@@ -92,10 +115,25 @@ public class QueueModeLwM2mRequestSender implements LwM2mRequestSender {
     @Override
     public <T extends LwM2mResponse> void send(final Registration destination, DownlinkRequest<T> request, long timeout,
             final ResponseCallback<T> responseCallback, final ErrorCallback errorCallback) {
+        send(destination, request, null, timeout, responseCallback, errorCallback);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public <T extends LwM2mResponse> void send(final Registration destination, DownlinkRequest<T> request,
+            LowerLayerConfig lowerLayerConfig, long timeout, final ResponseCallback<T> responseCallback,
+            final ErrorCallback errorCallback) {
 
         // If the client does not use Q-Mode, just send
         if (!destination.usesQueueMode()) {
-            delegatedSender.send(destination, request, timeout, responseCallback, errorCallback);
+            if (delegatedSender instanceof LwM2mRequestSender2) {
+                ((LwM2mRequestSender2) delegatedSender).send(destination, request, lowerLayerConfig, timeout,
+                        responseCallback, errorCallback);
+            } else {
+                delegatedSender.send(destination, request, timeout, responseCallback, errorCallback);
+            }
             return;
         }
 
@@ -107,31 +145,58 @@ public class QueueModeLwM2mRequestSender implements LwM2mRequestSender {
         }
 
         // Use delegation to send the request, with specific callbacks to perform Queue Mode operation
-        delegatedSender.send(destination, request, timeout, new ResponseCallback<T>() {
-            @Override
-            public void onResponse(T response) {
-                // Set the client awake. This will restart the timer.
-                presenceService.setAwake(destination);
-
-                // Call the user's callback
-                responseCallback.onResponse(response);
-            }
-        }, new ErrorCallback() {
-            @Override
-            public void onError(Exception e) {
-                if (e instanceof TimeoutException) {
-                    // If the timeout expires, this means the client does not respond.
-                    presenceService.setSleeping(destination);
-                } else if (e instanceof UnconnectedPeerException) {
-                    // if peer is not connected (No DTLS connection available)
-                    presenceService.setSleeping(destination);
+        if (delegatedSender instanceof LwM2mRequestSender2) {
+            ((LwM2mRequestSender2) delegatedSender).send(destination, request, lowerLayerConfig, timeout,
+                    new ResponseCallback<T>() {
+                        @Override
+                        public void onResponse(T response) {
+                            // Set the client awake. This will restart the timer.
+                            presenceService.setAwake(destination);
+
+                            // Call the user's callback
+                            responseCallback.onResponse(response);
+                        }
+                    }, new ErrorCallback() {
+                        @Override
+                        public void onError(Exception e) {
+                            if (e instanceof TimeoutException) {
+                                // If the timeout expires, this means the client does not respond.
+                                presenceService.setSleeping(destination);
+                            } else if (e instanceof UnconnectedPeerException) {
+                                // if peer is not connected (No DTLS connection available)
+                                presenceService.setSleeping(destination);
+                            }
+
+                            // Call the user's callback
+                            errorCallback.onError(e);
+                        }
+                    });
+        } else {
+            delegatedSender.send(destination, request, timeout, new ResponseCallback<T>() {
+                @Override
+                public void onResponse(T response) {
+                    // Set the client awake. This will restart the timer.
+                    presenceService.setAwake(destination);
+
+                    // Call the user's callback
+                    responseCallback.onResponse(response);
                 }
-
-                // Call the user's callback
-                errorCallback.onError(e);
-            }
-        });
-
+            }, new ErrorCallback() {
+                @Override
+                public void onError(Exception e) {
+                    if (e instanceof TimeoutException) {
+                        // If the timeout expires, this means the client does not respond.
+                        presenceService.setSleeping(destination);
+                    } else if (e instanceof UnconnectedPeerException) {
+                        // if peer is not connected (No DTLS connection available)
+                        presenceService.setSleeping(destination);
+                    }
+
+                    // Call the user's callback
+                    errorCallback.onError(e);
+                }
+            });
+        }
     }
 
     @Override
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/request/LowerLayerConfig.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/request/LowerLayerConfig.java
new file mode 100644
index 00000000..f14abdaf
--- /dev/null
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/request/LowerLayerConfig.java
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.request;
+
+/**
+ * Allow to apply custom configuration to lower layer.
+ * <p>
+ * If you are using LWM2M over CoAP this will help you to apply CoAP setting to you request.
+ * 
+ * @since 1.2
+ */
+public interface LowerLayerConfig {
+
+    /**
+     * @param lowerRequest the lower layer request on which the configuration should be applied. E.g. could be a CoAP
+     *        request.
+     */
+    void apply(Object lowerRequest);
+}
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/request/LwM2mRequestSender.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/request/LwM2mRequestSender.java
index fff15340..c0c8d158 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/request/LwM2mRequestSender.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/request/LwM2mRequestSender.java
@@ -32,7 +32,10 @@ import org.eclipse.leshan.server.registration.Registration;
 
 /**
  * A {@link LwM2mRequestSender} is responsible to send LWM2M {@link DownlinkRequest} for a given {@link Registration}.
+ * 
+ * @deprecated use {@link LwM2mRequestSender2} instead
  */
+@Deprecated
 public interface LwM2mRequestSender {
 
     /**
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/request/LwM2mRequestSender2.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/request/LwM2mRequestSender2.java
new file mode 100644
index 00000000..a75589e3
--- /dev/null
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/request/LwM2mRequestSender2.java
@@ -0,0 +1,99 @@
+/*******************************************************************************
+ * Copyright (c) 2013-2015 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *     Bosch Software Innovations GmbH - extension of ticket based asynchronous call.
+ *******************************************************************************/
+package org.eclipse.leshan.server.request;
+
+import org.eclipse.leshan.core.node.codec.CodecException;
+import org.eclipse.leshan.core.request.DownlinkRequest;
+import org.eclipse.leshan.core.request.exception.ClientSleepingException;
+import org.eclipse.leshan.core.request.exception.InvalidResponseException;
+import org.eclipse.leshan.core.request.exception.RequestCanceledException;
+import org.eclipse.leshan.core.request.exception.RequestRejectedException;
+import org.eclipse.leshan.core.request.exception.SendFailedException;
+import org.eclipse.leshan.core.request.exception.TimeoutException;
+import org.eclipse.leshan.core.request.exception.UnconnectedPeerException;
+import org.eclipse.leshan.core.response.ErrorCallback;
+import org.eclipse.leshan.core.response.LwM2mResponse;
+import org.eclipse.leshan.core.response.ResponseCallback;
+import org.eclipse.leshan.server.registration.Registration;
+
+/**
+ * A recent version of {@link LwM2mRequestSender} which add support to {@link LowerLayerConfig}
+ * 
+ * @since 1.2
+ */
+@SuppressWarnings("deprecation")
+public interface LwM2mRequestSender2 extends LwM2mRequestSender {
+
+    /**
+     * Send a Lightweight M2M request synchronously. Will block until a response is received from the remote server.
+     * <p>
+     * The synchronous way could block a thread during a long time so it is more recommended to use the asynchronous
+     * way.
+     * 
+     * @param destination The {@link Registration} associate to the device we want to sent the request.
+     * @param request The request to send to the client.
+     * @param lowerLayerConfig to tweak lower layer request (e.g. coap request)
+     * @param timeoutInMs The global timeout to wait in milliseconds (see
+     *        https://github.com/eclipse/leshan/wiki/Request-Timeout)
+     * @param <T> The expected type of the response received.
+     * @return the LWM2M response. The response can be <code>null</code> if the timeout expires (see
+     *         https://github.com/eclipse/leshan/wiki/Request-Timeout).
+     * 
+     * @throws CodecException if request payload can not be encoded.
+     * @throws InterruptedException if the thread was interrupted.
+     * @throws RequestRejectedException if the request is rejected by foreign peer.
+     * @throws RequestCanceledException if the request is cancelled.
+     * @throws SendFailedException if the request can not be sent. E.g. error at CoAP or DTLS/UDP layer.
+     * @throws InvalidResponseException if the response received is malformed.
+     * @throws UnconnectedPeerException if client is not connected (no dtls connection available).
+     * @throws ClientSleepingException if client is currently sleeping.
+     */
+    <T extends LwM2mResponse> T send(Registration destination, DownlinkRequest<T> request,
+            LowerLayerConfig lowerLayerConfig, long timeoutInMs) throws InterruptedException;
+
+    /**
+     * Send a Lightweight M2M {@link DownlinkRequest} asynchronously to a LWM2M client.
+     * 
+     * {@link ResponseCallback} and {@link ErrorCallback} are exclusively called.
+     * 
+     * @param destination The {@link Registration} associate to the device we want to sent the request.
+     * @param request The request to send to the client.
+     * @param lowerLayerConfig to tweak lower layer request (e.g. coap request)
+     * @param timeoutInMs The global timeout to wait in milliseconds (see
+     *        https://github.com/eclipse/leshan/wiki/Request-Timeout)
+     * @param responseCallback a callback called when a response is received (successful or error response). This
+     *        callback MUST NOT be null.
+     * @param <T> The expected type of the response received.
+     * @param errorCallback a callback called when an error or exception occurred when response is received. It can be :
+     *        <ul>
+     *        <li>{@link RequestRejectedException} if the request is rejected by foreign peer.</li>
+     *        <li>{@link RequestCanceledException} if the request is cancelled.</li>
+     *        <li>{@link SendFailedException} if the request can not be sent. E.g. error at CoAP or DTLS/UDP layer.</li>
+     *        <li>{@link InvalidResponseException} if the response received is malformed.</li>
+     *        <li>{@link UnconnectedPeerException} if client is not connected (no dtls connection available).</li>
+     *        <li>{@link ClientSleepingException} if client is currently sleeping.</li>
+     *        <li>{@link TimeoutException} if the timeout expires (see
+     *        https://github.com/eclipse/leshan/wiki/Request-Timeout).</li>
+     *        <li>or any other RuntimeException for unexpected issue.
+     *        </ul>
+     *        This callback MUST NOT be null.
+     * @throws CodecException if request payload can not be encoded.
+     */
+    <T extends LwM2mResponse> void send(Registration destination, DownlinkRequest<T> request,
+            LowerLayerConfig lowerLayerConfig, long timeoutInMs, ResponseCallback<T> responseCallback,
+            ErrorCallback errorCallback);
+}


Test patch:
diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilderTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilderTest.java
index b13f1cad..d41e556e 100644
--- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilderTest.java
+++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilderTest.java
@@ -90,7 +90,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(reg.getIdentity(), reg.getRootPath(), reg.getId(),
-                reg.getEndpoint(), model, encoder, false);
+                reg.getEndpoint(), model, encoder, false, null);
         ReadRequest request = new ReadRequest(3, 0);
         builder.visit(request);
 
@@ -108,7 +108,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(reg.getIdentity(), reg.getRootPath(), reg.getId(),
-                reg.getEndpoint(), model, encoder, false);
+                reg.getEndpoint(), model, encoder, false, null);
         ReadRequest request = new ReadRequest(3, 0, 1);
         builder.visit(request);
 
@@ -123,7 +123,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(reg.getIdentity(), reg.getRootPath(), reg.getId(),
-                reg.getEndpoint(), model, encoder, false);
+                reg.getEndpoint(), model, encoder, false, null);
         ReadRequest request = new ReadRequest(3);
         builder.visit(request);
 
@@ -138,7 +138,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(reg.getIdentity(), reg.getRootPath(), reg.getId(),
-                reg.getEndpoint(), model, encoder, false);
+                reg.getEndpoint(), model, encoder, false, null);
         DiscoverRequest request = new DiscoverRequest(3, 0);
         builder.visit(request);
 
@@ -157,7 +157,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(reg.getIdentity(), reg.getRootPath(), reg.getId(),
-                reg.getEndpoint(), model, encoder, false);
+                reg.getEndpoint(), model, encoder, false, null);
         WriteRequest request = new WriteRequest(Mode.UPDATE, 3, 0, LwM2mSingleResource.newStringResource(15, "value"));
         builder.visit(request);
 
@@ -181,7 +181,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(reg.getIdentity(), reg.getRootPath(), reg.getId(),
-                reg.getEndpoint(), model, encoder, false);
+                reg.getEndpoint(), model, encoder, false, null);
         WriteRequest request = new WriteRequest(3, 0, 14, "value");
         builder.visit(request);
 
@@ -196,7 +196,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(reg.getIdentity(), reg.getRootPath(), reg.getId(),
-                reg.getEndpoint(), model, encoder, false);
+                reg.getEndpoint(), model, encoder, false, null);
         AttributeSet attributes = new AttributeSet(new Attribute(Attribute.MINIMUM_PERIOD, 10L),
                 new Attribute(Attribute.MAXIMUM_PERIOD, 100L));
         WriteAttributesRequest request = new WriteAttributesRequest(3, 0, 14, attributes);
@@ -216,7 +216,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(reg.getIdentity(), reg.getRootPath(), reg.getId(),
-                reg.getEndpoint(), model, encoder, false);
+                reg.getEndpoint(), model, encoder, false, null);
         AttributeSet attributes = new AttributeSet(new Attribute(Attribute.MINIMUM_PERIOD),
                 new Attribute(Attribute.MAXIMUM_PERIOD));
         WriteAttributesRequest request = new WriteAttributesRequest(3, 0, 14, attributes);
@@ -236,7 +236,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(reg.getIdentity(), reg.getRootPath(), reg.getId(),
-                reg.getEndpoint(), model, encoder, false);
+                reg.getEndpoint(), model, encoder, false, null);
         ExecuteRequest request = new ExecuteRequest(3, 0, 12, "params");
         builder.visit(request);
 
@@ -255,7 +255,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(reg.getIdentity(), reg.getRootPath(), reg.getId(),
-                reg.getEndpoint(), model, encoder, false);
+                reg.getEndpoint(), model, encoder, false, null);
         CreateRequest request = new CreateRequest(12, LwM2mSingleResource.newStringResource(0, "value"));
         builder.visit(request);
 
@@ -278,7 +278,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(reg.getIdentity(), reg.getRootPath(), reg.getId(),
-                reg.getEndpoint(), model, encoder, false);
+                reg.getEndpoint(), model, encoder, false, null);
         CreateRequest request = new CreateRequest(12,
                 new LwM2mObjectInstance(26, LwM2mSingleResource.newStringResource(0, "value")));
         builder.visit(request);
@@ -303,7 +303,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(reg.getIdentity(), reg.getRootPath(), reg.getId(),
-                reg.getEndpoint(), model, encoder, false);
+                reg.getEndpoint(), model, encoder, false, null);
         DeleteRequest request = new DeleteRequest(12, 0);
         builder.visit(request);
 
@@ -321,7 +321,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(reg.getIdentity(), reg.getRootPath(), reg.getId(),
-                reg.getEndpoint(), model, encoder, false);
+                reg.getEndpoint(), model, encoder, false, null);
         ObserveRequest request = new ObserveRequest(12, 0);
         builder.visit(request);